From 0fbc1ad47c9a163e78377b6680be51830b9c3a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=9F=E5=B8=85?= <133814250@qq.com> Date: Thu, 7 Mar 2024 20:08:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83v2.13.1=E7=89=88=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E8=AF=B7=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=EF=BC=9Ahttps://github.com/bufanyun/hotgo/blob/v2.0/d?= =?UTF-8?q?ocs/guide-zh-CN/start-update-log.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +- docs/guide-zh-CN/README.md | 5 +- docs/guide-zh-CN/addon-flow.md | 5 +- docs/guide-zh-CN/addon-helper.md | 14 +- docs/guide-zh-CN/images/sys-library-dict.png | Bin 0 -> 99535 bytes docs/guide-zh-CN/start-environment.md | 4 +- docs/guide-zh-CN/start-installation.md | 2 +- docs/guide-zh-CN/start-update-log.md | 14 + docs/guide-zh-CN/sys-auth.md | 2 +- docs/guide-zh-CN/sys-library.md | 154 +++ docs/guide-zh-CN/sys-payment.md | 13 +- docs/guide-zh-CN/sys-tcp-server.md | 2 +- docs/guide-zh-CN/sys-webhook.md | 2 +- docs/guide-zh-CN/sys-websocket-client.md | 210 ++++ docs/guide-zh-CN/sys-websocket-server.md | 143 +++ docs/guide-zh-CN/web-form.md | 28 + server/addons/addons.go | 6 +- .../addons/hgexample/api/admin/comp/import.go | 26 + .../hgexample/controller/admin/sys/comp.go | 46 + .../controller/websocket/handler/index.go | 25 + server/addons/hgexample/main.go | 31 +- server/addons/hgexample/router/admin.go | 1 + server/addons/hgexample/router/websocket.go | 3 +- server/addons/modules/addons.go | 1 - server/api/admin/addons/addons.go | 9 - server/api/admin/blacklist/blacklist.go | 2 +- server/api/admin/common/upload.go | 20 + server/api/admin/creditslog/creditslog.go | 10 - server/api/admin/curddemo/curddemo.go | 4 +- server/api/admin/order/order.go | 11 - server/go.mod | 26 +- server/go.sum | 52 +- server/internal/cmd/http.go | 46 +- server/internal/consts/addons.go | 50 +- server/internal/consts/cache.go | 5 +- server/internal/consts/credit_log.go | 77 +- server/internal/consts/order.go | 111 +-- server/internal/consts/pay.go | 32 +- server/internal/consts/version.go | 2 +- .../controller/admin/admin/credits_log.go | 10 - .../internal/controller/admin/admin/order.go | 11 - .../controller/admin/common/upload.go | 22 + .../internal/controller/admin/sys/addons.go | 12 - .../controller/admin/sys/curd_demo.go | 9 +- server/internal/library/addons/addons.go | 35 +- server/internal/library/addons/build.go | 61 +- .../internal/library/addons/build_layout.go | 35 + server/internal/library/addons/module.go | 73 +- server/internal/library/cache/cache.go | 23 +- server/internal/library/dict/dict.go | 61 ++ server/internal/library/dict/dict_option.go | 106 +++ .../library/dict/dict_register_enums.go | 72 ++ .../library/dict/dict_register_func.go | 131 +++ .../library/hggen/internal/cmd/cmd_build.go | 144 +-- .../library/hggen/internal/cmd/cmd_run.go | 24 +- .../internal/cmd/cmd_z_unit_build_test.go | 151 +++ .../internal/cmd/cmd_z_unit_gen_ctrl_test.go | 5 +- .../internal/cmd/cmd_z_unit_gen_dao_test.go | 259 +++++ .../cmd/cmd_z_unit_gen_service_test.go | 5 +- .../hggen/internal/cmd/gendao/gendao.go | 10 + .../hggen/internal/cmd/gendao/gendao_clear.go | 38 +- .../hggen/internal/cmd/gendao/gendao_dao.go | 7 +- .../hggen/internal/cmd/gendao/gendao_do.go | 5 +- .../internal/cmd/gendao/gendao_entity.go | 6 +- .../internal/cmd/gendao/gendao_gen_item.go | 53 ++ .../internal/cmd/genservice/genservice.go | 2 +- .../cmd/genservice/genservice_calculate.go | 16 +- .../cmd/genservice/genservice_generate.go | 25 +- .../cmd/testdata/build/multiple/multiple.go | 5 + .../cmd/testdata/build/single/main.go | 5 + .../internal/cmd/testdata/build/varmap/go.mod | 12 + .../internal/cmd/testdata/build/varmap/go.sum | 27 + .../cmd/testdata/build/varmap/main.go | 13 + .../genservice/logic/article/article.go | 1 + .../cmd/testdata/issue/2572/config.yaml | 20 + .../issue/2572/dao/internal/user_3.go | 85 ++ .../issue/2572/dao/internal/user_4.go | 85 ++ .../cmd/testdata/issue/2572/dao/user_3.go | 27 + .../cmd/testdata/issue/2572/dao/user_4.go | 27 + .../testdata/issue/2572/model/do/user_3.go | 22 + .../testdata/issue/2572/model/do/user_4.go | 22 + .../issue/2572/model/entity/user_3.go | 20 + .../issue/2572/model/entity/user_4.go | 20 + .../internal/cmd/testdata/issue/2572/sql1.sql | 10 + .../internal/cmd/testdata/issue/2572/sql2.sql | 10 + .../cmd/testdata/issue/2616/config.yaml | 20 + .../issue/2616/dao/internal/user_1.go | 85 ++ .../issue/2616/dao/internal/user_2.go | 85 ++ .../issue/2616/dao/internal/user_3.go | 85 ++ .../issue/2616/dao/internal/user_4.go | 85 ++ .../cmd/testdata/issue/2616/dao/user_1.go | 29 + .../cmd/testdata/issue/2616/dao/user_2.go | 29 + .../cmd/testdata/issue/2616/dao/user_3.go | 27 + .../cmd/testdata/issue/2616/dao/user_4.go | 27 + .../testdata/issue/2616/model/do/user_3.go | 22 + .../testdata/issue/2616/model/do/user_4.go | 22 + .../issue/2616/model/entity/user_3.go | 20 + .../issue/2616/model/entity/user_4.go | 20 + .../internal/cmd/testdata/issue/2616/sql1.sql | 10 + .../internal/cmd/testdata/issue/2616/sql2.sql | 10 + .../cmd/testdata/issue/2746/issue_2746.go | 18 + .../internal/cmd/testdata/issue/2746/sql.sql | 9 + .../hggen/internal/utility/utils/utils.go | 1 + server/internal/library/hggen/views/curd.go | 13 + .../hggen/views/curd_generate_web_model.go | 57 +- server/internal/library/hggen/views/utils.go | 45 + server/internal/library/location/location.go | 2 +- .../library/network/tcp/tcp_example_test.go | 5 + server/internal/library/storager/model.go | 66 +- server/internal/library/storager/upload.go | 197 +++- .../internal/library/storager/upload_cos.go | 12 + .../internal/library/storager/upload_local.go | 103 ++ .../internal/library/storager/upload_minio.go | 12 + .../internal/library/storager/upload_oss.go | 12 + .../internal/library/storager/upload_qiniu.go | 12 + .../library/storager/upload_ucloud.go | 12 + server/internal/logic/admin/monitor.go | 5 + server/internal/logic/admin/post.go | 22 + server/internal/logic/admin/site.go | 13 +- server/internal/logic/common/upload.go | 22 + server/internal/logic/hook/access_log.go | 2 +- server/internal/logic/hook/last_active.go | 7 +- server/internal/logic/middleware/home_auth.go | 5 + server/internal/logic/middleware/init.go | 24 +- .../logic/middleware/limit_blacklist.go | 5 + .../logic/middleware/limit_develop.go | 5 + .../internal/logic/middleware/pre_filter.go | 21 +- server/internal/logic/middleware/response.go | 10 + server/internal/logic/pay/create.go | 5 + server/internal/logic/pay/notify.go | 13 + server/internal/logic/sys/addons.go | 40 +- server/internal/logic/sys/blacklist.go | 4 +- server/internal/logic/sys/config.go | 6 - server/internal/logic/sys/curd_demo.go | 4 +- server/internal/logic/sys/dict_data.go | 11 + server/internal/logic/sys/dict_type.go | 73 +- server/internal/logic/sys/log.go | 4 + .../logic/tcpserver/example_handle.go | 5 + server/internal/model/config_load.go | 6 - server/internal/model/dict.go | 16 + server/internal/model/input/adminin/order.go | 4 +- .../internal/model/input/commonin/wechat.go | 5 + server/internal/model/input/sysin/addons.go | 10 +- .../internal/model/input/sysin/attachment.go | 18 + .../internal/model/input/sysin/curd_demo.go | 4 +- .../internal/model/input/sysin/dict_data.go | 12 +- .../internal/model/input/sysin/provinces.go | 1 + server/internal/router/genrouter/curd_demo.go | 4 +- server/internal/service/admin.go | 408 ++++---- server/internal/service/common.go | 4 + server/internal/service/middleware.go | 26 +- server/internal/service/pay.go | 40 +- server/internal/service/sys.go | 800 ++++++++-------- server/internal/websocket/client.go | 2 +- server/internal/websocket/client_manager.go | 11 +- server/manifest/config/config.example.yaml | 2 +- server/resource/addons/.gitkeep | 0 .../addons/hgexample}/public/default | 0 .../hgexample}/template/home/index.html | 0 .../generate/default/addon/main.go.template | 31 +- server/storage/data/hotgo.sql | 899 +++++++++--------- server/utility/convert/convert.go | 13 - server/utility/convert/slice.go | 59 ++ server/utility/validate/filter_test.go | 5 + server/utility/validate/validate_test.go | 10 +- web/package.json | 9 +- web/src/api/addons/hgexample/comp/index.ts | 13 + web/src/api/base/index.ts | 21 + web/src/api/develop/addons.ts | 7 - web/src/components/FileChooser/src/Upload.vue | 3 +- .../components/IconSelector/AntdSelector.vue | 5 +- web/src/components/IconSelector/index.vue | 7 +- web/src/components/Upload/multipartUpload.vue | 197 ++++ web/src/enums/socketEnum.ts | 9 +- web/src/hooks/useCreateScript.ts | 21 + web/src/layout/components/Footer/index.ts | 3 - web/src/layout/components/Footer/index.vue | 56 -- web/src/layout/index.vue | 4 - web/src/main.ts | 12 +- web/src/utils/highHtml.ts | 186 ++++ web/src/utils/hotgo.ts | 14 + web/src/utils/http/axios/Axios.ts | 75 +- .../{websocket.ts => websocket/index.ts} | 185 ++-- web/src/utils/websocket/registerMessage.ts | 32 + .../addons/hgexample/comp/calendar/index.vue | 42 + .../views/addons/hgexample/comp/des/index.vue | 89 ++ .../addons/hgexample/comp/directive/index.vue | 58 ++ .../addons/hgexample/comp/drag/index.vue | 162 ++++ .../hgexample/comp/fingerprintjs/index.vue | 38 + .../addons/hgexample/comp/form/basic.vue | 175 ++++ .../addons/hgexample/comp/form/useForm.vue | 198 ++++ .../addons/hgexample/comp/icons/antd.vue | 23 + .../addons/hgexample/comp/icons/icons.vue | 136 +++ .../addons/hgexample/comp/icons/ionicons5.vue | 23 + .../addons/hgexample/comp/icons/selector.vue | 29 + .../addons/hgexample/comp/import/excel.vue | 68 ++ web/src/views/addons/hgexample/comp/index.vue | 369 +++++++ .../views/addons/hgexample/comp/map/baidu.vue | 33 + .../views/addons/hgexample/comp/map/gaode.vue | 34 + .../addons/hgexample/comp/modal/index.vue | 307 ++++++ .../hgexample/comp/moreComponents/index.vue | 15 + .../addons/hgexample/comp/notice/index.vue | 322 +++++++ .../addons/hgexample/comp/print/index.vue | 109 +++ .../addons/hgexample/comp/result/fail.vue | 70 ++ .../addons/hgexample/comp/result/info.vue | 74 ++ .../addons/hgexample/comp/result/success.vue | 55 ++ .../addons/hgexample/comp/tagsView/index.vue | 26 + .../hgexample/comp/text/gradient/index.vue | 70 ++ .../addons/hgexample/comp/text/high/index.vue | 94 ++ .../addons/hgexample/comp/text/mint/index.vue | 50 + .../hgexample/comp/text/pinyin/index.vue | 102 ++ .../addons/hgexample/comp/timeline/index.vue | 97 ++ .../addons/hgexample/comp/vis/echarts.vue | 15 + .../addons/hgexample/comp/vis/madeapie.vue | 15 + .../addons/hgexample/comp/vis/ppchart.vue | 15 + .../addons/hgexample/comp/waterfall/index.vue | 106 +++ .../addons/hgexample/comp/watermark/index.vue | 171 ++++ .../views/addons/hgexample/portal/form.vue | 4 +- .../views/addons/hgexample/portal/index.vue | 4 +- .../addons/hgexample/portal/websocketTest.vue | 134 +++ web/src/views/apply/attachment/index.vue | 42 +- web/src/views/apply/notice/index.vue | 2 +- web/src/views/asset/cash/list.vue | 2 +- web/src/views/asset/creditsLog/list.vue | 2 +- web/src/views/asset/creditsLog/model.ts | 5 +- web/src/views/asset/rechargeLog/index.vue | 2 +- web/src/views/asset/rechargeLog/list.vue | 2 +- web/src/views/asset/rechargeLog/model.ts | 13 +- web/src/views/curdDemo/model.ts | 9 +- .../views/develop/addons/components/model.ts | 23 - web/src/views/develop/addons/index.vue | 237 +---- web/src/views/develop/addons/model.ts | 203 ++++ .../code/components/EditMasterCell.vue | 13 +- web/src/views/develop/code/index.vue | 2 +- web/src/views/iframe/index.vue | 8 + web/src/views/log/log/index.vue | 2 +- web/src/views/log/login-log/index.vue | 4 +- web/src/views/log/sms-log/index.vue | 2 +- web/src/views/monitor/netconn/index.vue | 2 +- web/src/views/monitor/online/index.vue | 2 +- web/src/views/monitor/serve-log/index.vue | 4 +- web/src/views/monitor/serve-log/model.ts | 10 +- web/src/views/monitor/serve-monitor/index.vue | 45 +- web/src/views/org/user/list.vue | 4 +- web/src/views/org/user/model.ts | 13 +- web/src/views/system/config/UploadSetting.vue | 8 +- 246 files changed, 9441 insertions(+), 2293 deletions(-) create mode 100644 docs/guide-zh-CN/images/sys-library-dict.png create mode 100644 docs/guide-zh-CN/sys-websocket-client.md create mode 100644 docs/guide-zh-CN/sys-websocket-server.md create mode 100644 server/addons/hgexample/api/admin/comp/import.go create mode 100644 server/addons/hgexample/controller/admin/sys/comp.go create mode 100644 server/addons/hgexample/controller/websocket/handler/index.go delete mode 100644 server/addons/modules/addons.go create mode 100644 server/internal/library/dict/dict.go create mode 100644 server/internal/library/dict/dict_option.go create mode 100644 server/internal/library/dict/dict_register_enums.go create mode 100644 server/internal/library/dict/dict_register_func.go create mode 100644 server/internal/library/hggen/internal/cmd/cmd_z_unit_build_test.go create mode 100644 server/internal/library/hggen/internal/cmd/gendao/gendao_gen_item.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/build/multiple/multiple.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/build/single/main.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/build/varmap/go.mod create mode 100644 server/internal/library/hggen/internal/cmd/testdata/build/varmap/go.sum create mode 100644 server/internal/library/hggen/internal/cmd/testdata/build/varmap/main.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/config.yaml create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/dao/internal/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/dao/internal/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/dao/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/dao/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/model/do/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/model/do/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/model/entity/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/model/entity/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/sql1.sql create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2572/sql2.sql create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/config.yaml create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/internal/user_1.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/internal/user_2.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/internal/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/internal/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/user_1.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/user_2.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/dao/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/model/do/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/model/do/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/model/entity/user_3.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/model/entity/user_4.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/sql1.sql create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2616/sql2.sql create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2746/issue_2746.go create mode 100644 server/internal/library/hggen/internal/cmd/testdata/issue/2746/sql.sql create mode 100644 server/internal/model/dict.go create mode 100644 server/resource/addons/.gitkeep rename server/{addons/hgexample/resource => resource/addons/hgexample}/public/default (100%) rename server/{addons/hgexample/resource => resource/addons/hgexample}/template/home/index.html (100%) create mode 100644 server/utility/convert/slice.go create mode 100644 web/src/api/addons/hgexample/comp/index.ts create mode 100644 web/src/components/Upload/multipartUpload.vue create mode 100644 web/src/hooks/useCreateScript.ts delete mode 100644 web/src/layout/components/Footer/index.ts delete mode 100644 web/src/layout/components/Footer/index.vue create mode 100644 web/src/utils/highHtml.ts rename web/src/utils/{websocket.ts => websocket/index.ts} (50%) create mode 100644 web/src/utils/websocket/registerMessage.ts create mode 100644 web/src/views/addons/hgexample/comp/calendar/index.vue create mode 100644 web/src/views/addons/hgexample/comp/des/index.vue create mode 100644 web/src/views/addons/hgexample/comp/directive/index.vue create mode 100644 web/src/views/addons/hgexample/comp/drag/index.vue create mode 100644 web/src/views/addons/hgexample/comp/fingerprintjs/index.vue create mode 100644 web/src/views/addons/hgexample/comp/form/basic.vue create mode 100644 web/src/views/addons/hgexample/comp/form/useForm.vue create mode 100644 web/src/views/addons/hgexample/comp/icons/antd.vue create mode 100644 web/src/views/addons/hgexample/comp/icons/icons.vue create mode 100644 web/src/views/addons/hgexample/comp/icons/ionicons5.vue create mode 100644 web/src/views/addons/hgexample/comp/icons/selector.vue create mode 100644 web/src/views/addons/hgexample/comp/import/excel.vue create mode 100644 web/src/views/addons/hgexample/comp/index.vue create mode 100644 web/src/views/addons/hgexample/comp/map/baidu.vue create mode 100644 web/src/views/addons/hgexample/comp/map/gaode.vue create mode 100644 web/src/views/addons/hgexample/comp/modal/index.vue create mode 100644 web/src/views/addons/hgexample/comp/moreComponents/index.vue create mode 100644 web/src/views/addons/hgexample/comp/notice/index.vue create mode 100644 web/src/views/addons/hgexample/comp/print/index.vue create mode 100644 web/src/views/addons/hgexample/comp/result/fail.vue create mode 100644 web/src/views/addons/hgexample/comp/result/info.vue create mode 100644 web/src/views/addons/hgexample/comp/result/success.vue create mode 100644 web/src/views/addons/hgexample/comp/tagsView/index.vue create mode 100644 web/src/views/addons/hgexample/comp/text/gradient/index.vue create mode 100644 web/src/views/addons/hgexample/comp/text/high/index.vue create mode 100644 web/src/views/addons/hgexample/comp/text/mint/index.vue create mode 100644 web/src/views/addons/hgexample/comp/text/pinyin/index.vue create mode 100644 web/src/views/addons/hgexample/comp/timeline/index.vue create mode 100644 web/src/views/addons/hgexample/comp/vis/echarts.vue create mode 100644 web/src/views/addons/hgexample/comp/vis/madeapie.vue create mode 100644 web/src/views/addons/hgexample/comp/vis/ppchart.vue create mode 100644 web/src/views/addons/hgexample/comp/waterfall/index.vue create mode 100644 web/src/views/addons/hgexample/comp/watermark/index.vue create mode 100644 web/src/views/addons/hgexample/portal/websocketTest.vue delete mode 100644 web/src/views/develop/addons/components/model.ts create mode 100644 web/src/views/develop/addons/model.ts diff --git a/README.md b/README.md index a0ac564..520d0d1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ ## 平台简介 -* 基于全新Go Frame 2+Vue3+Naive UI+uniapp开发的全栖框架,为二次开发而生,适合中小型完整应用开发。 +* 基于全新GoFrame2+Vue3+NaiveUI+uniapp开发的全栖框架,为二次开发而生,适合中小型完整应用开发。 * 前端采用Naive-Ui-Admin、Vue、Naive UI、uniapp。 ## 演示地址 @@ -42,21 +42,21 @@ ## 特征 * 高生产率:极强的可扩展性,应用化、模块化、插件化机制敏捷开发,几分钟即可搭建一个应用开发骨架。 -* 多应用入口:多入口分为 Admin (后台)、Home (前台页面)、Api (对外通用接口)、Websocket (即时通讯接口),不同的业务,进入不同的应用入口。 +* 多应用入口:多入口分为 Admin (后台)、Home (前台页面)、Api (对外通用接口)、WebSocket (即时通讯接口),不同的业务,进入不同的应用入口。 * 极致的插件化: 微核架构,功能隔离,高可定制性,可以渐进式开发,亦可以多人协同开发。支持一键创建插件模板、一键安装、更新、卸载插件、可以非常方便的将插件迁移到新项目中。 * 快速生成代码:无需编写代码,只需创建表进行简单配置就能生成一个完善的 CURD、树表等常用的开发代码,其中所需表单控件也是勾选即可直接生成。 * 认证机制:采用 JWT 的用户状态认证及 casbin 的权限认证 -* 路由模式:得益于 goframe2.0 提供了规范化的路由注册方式,无需注解自动生成api文档 +* 路由模式:得益于 GoFrame 提供了规范化的路由注册方式,无需注解自动生成api文档 * 模块化设计,面向接口开发 -## 后台内置功能 +## 内置功能 1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 2. 部门管理:配置系统组织机构(公司、部门、岗位),树结构展现支持数据权限。 3. 岗位管理:配置系统用户所属担任职务。 4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 5. 角色管理:角色菜单权限分配、设置角色按机构或按上下级关系进行数据范围权限划分。 -6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 +6. 字典管理:对系统中经常使用的一些特定数据进行维护,支持枚举字典和自定义方法字典。 7. 配置管理:对系统动态配置常用参数。 8. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 9. 登录日志:系统登录日志记录查询包含登录异常。 @@ -68,10 +68,10 @@ 15. 代码生成:支持自动化生成前后端代码。CURD关联表、树表、消息队列、定时任务一键生成等。 16. 插件应用:支持一键生成插件模板,每个插件之间开发隔离,拥有独立多应用入口、独立配置。完美支持多人协同开发、插件插拔不会对原系统产生影响等。 17. 服务监控:监视当前系统CPU、内存、磁盘、网络、堆栈等相关信息。 -18. 附件管理:文件图片上传,支持本地、阿里云oss、腾讯云cos、ucloud对象存储、七牛云对象存储、minio等多种上传驱动,后台一键切换配置,并集成了文件选择器。 +18. 附件管理:文件图片上传,大文件分片上传、断点续传,支持本地、阿里云oss、腾讯云cos、ucloud对象存储、七牛云对象存储、minio等多种上传驱动,后台一键切换配置,并集成了文件选择器。 19. TCP服务:基于gtcp的服务应用,支持长连接、断线重连、服务认证、路由分发、RPC消息、拦截器和数据绑定等。简化和规范了服务器开发流程。 20. 消息队列:同时兼容 kafka、redis、rocketmq、磁盘队列,一键配置切换到场景适用的MQ。 -21. 通知公告:采用websocket实时推送在线用户最新通知、公告、私信消息。 +21. 通知公告:采用WebSocket实时推送在线用户最新通知、公告、私信消息。 22. 地区编码:整合国内通用省市区编码,运用于项目于一身,支持动态省市区选项。 23. 常用工具:集成常用的工具包和命令行工具,可以快速开发自定义命令行,多种启动入口。 @@ -132,7 +132,7 @@ * 本项目包含的第三方源码和二进制文件之版权信息另行标注。 -* 版权所有Copyright © 2020-2023 by Ms (https://github.com/bufanyun/hotgo) +* 版权所有Copyright © 2020-2024 by Ms (https://github.com/bufanyun/hotgo) * All rights reserved。 @@ -154,7 +154,7 @@ ## License -[MIT © HotGo-2023](./LICENSE) +[MIT © HotGo-2024](./LICENSE) diff --git a/docs/guide-zh-CN/README.md b/docs/guide-zh-CN/README.md index 362243f..35d8450 100644 --- a/docs/guide-zh-CN/README.md +++ b/docs/guide-zh-CN/README.md @@ -26,7 +26,7 @@ - [消息队列](sys-queue.md) - [功能扩展库](sys-library.md) - [工具方法](sys-utility.md) -- Websocket服务器 +- [WebSocket服务器](sys-websocket-server.md) - [TCP服务器](sys-tcp-server.md) - [单元测试](sys-test.md) @@ -40,8 +40,7 @@ ### 前端开发 - [表单组件](web-form.md) -- Websocket客户端 -- 工具库 +- [WebSocket客户端](sys-websocket-client.md) - [独立部署](web-deploy.md) diff --git a/docs/guide-zh-CN/addon-flow.md b/docs/guide-zh-CN/addon-flow.md index eeed751..35650e4 100644 --- a/docs/guide-zh-CN/addon-flow.md +++ b/docs/guide-zh-CN/addon-flow.md @@ -19,8 +19,9 @@ 1. /server/addons/hgexample/ # 插件模块目录 2. /server/addons/modules/hgexample.go # 隐式注册插件文件 -3. /web/src/api/addons/hgexample # webApi目录 -4. /web/src/views/addons/hgexample # web页面目录 +3. /server/resource/addons/hgexample # 静态资源和页面模板目录,属于扩展功能选项,勾选对应选项后才会生成 +4. /web/src/api/addons/hgexample # webApi目录 +5. /web/src/views/addons/hgexample # web页面目录 # 默认情况下没有为web页面生成菜单权限,因为在实际场景中插件不一定需要用到web页面,所以如有需要请手动到后台 权限管理 -> 菜单权限->自行添加菜单和配置权限 ``` diff --git a/docs/guide-zh-CN/addon-helper.md b/docs/guide-zh-CN/addon-helper.md index cd1101c..2ae1465 100644 --- a/docs/guide-zh-CN/addon-helper.md +++ b/docs/guide-zh-CN/addon-helper.md @@ -30,13 +30,13 @@ func (s *Skeleton) GetModule() Module { // Module 插件模块 type Module interface { - Init(ctx context.Context) // 初始化 - InitRouter(ctx context.Context, group *ghttp.RouterGroup) // 初始化并注册路由 - Ctx() context.Context // 上下文 - GetSkeleton() *Skeleton // 架子 - Install(ctx context.Context) error // 安装模块 - Upgrade(ctx context.Context) error // 更新模块 - UnInstall(ctx context.Context) error // 卸载模块 + Start(option *Option) (err error) // 启动模块 + Stop() (err error) // 停止模块 + Ctx() context.Context // 上下文 + GetSkeleton() *Skeleton // 获取模块 + Install(ctx context.Context) (err error) // 安装模块 + Upgrade(ctx context.Context) (err error) // 更新模块 + UnInstall(ctx context.Context) (err error) // 卸载模块 } ``` diff --git a/docs/guide-zh-CN/images/sys-library-dict.png b/docs/guide-zh-CN/images/sys-library-dict.png new file mode 100644 index 0000000000000000000000000000000000000000..421973f20de3e0ebb5b163332347814535ffcb5c GIT binary patch literal 99535 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

%h;;5%AEvoB(+_ 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 需要本地具有 git node golang 环境 - node版本 >= 16.0.0 -- golang版本 >= v1.19 -- mysql 引擎需要是 innoDB +- golang版本 >= 1.19 +- mysql版本 >= 5.7,引擎需要是 innoDB - IDE推荐:Goland diff --git a/docs/guide-zh-CN/start-installation.md b/docs/guide-zh-CN/start-installation.md index 836e0e1..1831f13 100644 --- a/docs/guide-zh-CN/start-installation.md +++ b/docs/guide-zh-CN/start-installation.md @@ -9,7 +9,7 @@ - node版本 >= v16.0.0 - golang版本 >= v1.19 -- goframe版本 >=v2.6.1 +- goframe版本 >=v2.6.4 - mysql版本 >=5.7 > 必须先看[环境搭建文档](start-environment.md),如果安装遇到问题务必先查看[常见问题文档](start-issue.md) diff --git a/docs/guide-zh-CN/start-update-log.md b/docs/guide-zh-CN/start-update-log.md index a02d730..8e9fe0b 100644 --- a/docs/guide-zh-CN/start-update-log.md +++ b/docs/guide-zh-CN/start-update-log.md @@ -11,6 +11,20 @@ > 如果升级(覆盖)代码后打开会出现 sql 报错, 请检查更新的数据库格式或自行调整 +### v2.13.1 +updated 2024.3.7 + +- 增加:增加内置数据字典类型:`枚举字典`和`自定义方法字典`,支持代码生成时关联选项使用 +- 增加:增加大文件上传,支持分片上传、断点续传,存储驱动已适配`本地存储` +- 增加:插件模块增加停止服务回调接口,调整静态资源默认存放位置,创建插件选项增加可选扩展功能 +- 增加:功能案例插件增加`30+`常用组件示例,增加`websocket`消息收发测试 +- 增加:文档增`加功能扩展库`、`websocket服务器`、`websocket客户端`使用说明,当前版本文档已完善 +- 修复:修复省市区无法添加地区问题 +- 优化:gf版本升级到v2.6.4 +- 优化:优化缓存组件依赖关系 +- 优化:调整部分前端表格自适应宽度 +- 优化:HTTP错误码接管统一改为由响应中间件处理 + ### v2.12.1 updated 2023.12.29 diff --git a/docs/guide-zh-CN/sys-auth.md b/docs/guide-zh-CN/sys-auth.md index ca32a34..27ac252 100644 --- a/docs/guide-zh-CN/sys-auth.md +++ b/docs/guide-zh-CN/sys-auth.md @@ -105,7 +105,7 @@ graph TD #### 如何区分部门和下级用户? - 在实际使用时,部门更多的是在公司或机构中使用,可以通过在 组织管理 -> 后台用户 ->为用户绑定部门 -- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户。后续也将开放邀请码绑定下级功能。 +- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户 #### 如何判断数据是谁的? diff --git a/docs/guide-zh-CN/sys-library.md b/docs/guide-zh-CN/sys-library.md index 1cb3d2c..e359633 100644 --- a/docs/guide-zh-CN/sys-library.md +++ b/docs/guide-zh-CN/sys-library.md @@ -5,6 +5,7 @@ - 缓存驱动 - 请求上下文 - JWT +- 数据字典 - 地理定位(待写) - 通知(待写) @@ -151,6 +152,159 @@ func test(ctx context.Context) { ``` +### 数据字典 + +- hotgo增加了对枚举字典和自定义方法字典的内置支持,从而在系统中经常使用的一些特定数据维护基础上做出了增强。 + +#### 字典数据选项 +- 文件路径:server/internal/model/dict.go +```go +package model + +// Option 字典数据选项 +type Option struct { + Key interface{} `json:"key"` + Label string `json:"label" description:"字典标签"` + Value interface{} `json:"value" description:"字典键值"` + ValueType string `json:"valueType" description:"键值数据类型"` + Type string `json:"type" description:"字典类型"` + ListClass string `json:"listClass" description:"表格回显样式"` +} +``` + +#### 枚举字典 +- 适用于系统开发期间内置的枚举数据,这样即维护了枚举值,又关联了数据字典 + +##### 一个例子 +- 定义枚举值和字典数据选项,并注册字典类型 +- 文件路径:server/internal/consts/credit_log.go + +```go +package consts + +import ( + "hotgo/internal/library/dict" + "hotgo/internal/model" +) + +func init() { + dict.RegisterEnums("creditType", "资金变动类型", CreditTypeOptions) + dict.RegisterEnums("creditGroup", "资金变动分组", CreditGroupOptions) +} + +const ( + CreditTypeBalance = "balance" // 余额 + CreditTypeIntegral = "integral" // 积分 +) + +const ( + CreditGroupDecr = "decr" // 扣款 + CreditGroupIncr = "incr" // 加款 + CreditGroupOpDecr = "op_decr" // 操作扣款 + CreditGroupOpIncr = "op_incr" // 操作加款 + CreditGroupBalanceRecharge = "balance_recharge" // 余额充值 + CreditGroupBalanceRefund = "balance_refund" // 余额退款 + CreditGroupApplyCash = "apply_cash" // 申请提现 +) + +// CreditTypeOptions 变动类型 +var CreditTypeOptions = []*model.Option{ + dict.GenSuccessOption(CreditTypeBalance, "余额"), + dict.GenInfoOption(CreditTypeIntegral, "积分"), +} + +// CreditGroupOptions 变动分组 +var CreditGroupOptions = []*model.Option{ + dict.GenWarningOption(CreditGroupDecr, "扣款"), + dict.GenSuccessOption(CreditGroupIncr, "加款"), + dict.GenWarningOption(CreditGroupOpDecr, "操作扣款"), + dict.GenSuccessOption(CreditGroupOpIncr, "操作加款"), + dict.GenWarningOption(CreditGroupBalanceRefund, "余额退款"), + dict.GenSuccessOption(CreditGroupBalanceRecharge, "余额充值"), + dict.GenInfoOption(CreditGroupApplyCash, "申请提现"), +} + +``` + + +#### 自定义方法字典 +- 适用于非固定选项,如数据是从某个表/文件读取或从第三方读取,数据需要进行转换时使用 + +##### 方法字典接口 +- 文件路径:server/internal/consts/credit_log.go +```go +package dict + +// FuncDict 方法字典,实现本接口即可使用内置方法字典 +type FuncDict func(ctx context.Context) (res []*model.Option, err error) +``` + +##### 一个例子 +- 定义获取字典数据方法,并注册字典类型 +- 文件路径:server/internal/logic/admin/post.go + +```go +package admin + +import ( + "context" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "hotgo/internal/consts" + "hotgo/internal/dao" + "hotgo/internal/library/dict" + "hotgo/internal/model" + "hotgo/internal/model/entity" + "hotgo/internal/service" +) + +type sAdminPost struct{} + +func NewAdminPost() *sAdminPost { + return &sAdminPost{} +} + +func init() { + service.RegisterAdminPost(NewAdminPost()) + dict.RegisterFunc("adminPostOption", "岗位选项", service.AdminPost().Option) +} + +// Option 岗位选项 +func (s *sAdminPost) Option(ctx context.Context) (opts []*model.Option, err error) { + var list []*entity.AdminPost + if err = dao.AdminPost.Ctx(ctx).OrderAsc(dao.AdminPost.Columns().Sort).Scan(&list); err != nil { + return nil, err + } + + if len(list) == 0 { + opts = make([]*model.Option, 0) + return + } + + for _, v := range list { + opts = append(opts, dict.GenHashOption(v.Id, v.Name)) + } + return +} +``` + +#### 代码生成支持 +- 内置的枚举字典和自定义方法字典在生成代码时可以直接进行选择,生成代码格式和系统字典管理写法一致 + +![最终编辑表单效果](images/sys-library-dict.png) + + +#### 内置字典和系统字典的区分 + +##### 主要区别 +- 系统字典由表:`hg_sys_dict_type`和`hg_sys_dict_data`共同进行维护,使用时需通过后台到字典管理中进行添加 +- 内置字典是系统开发期间在代码层面事先定义和注册好的数据选项 + + +##### 数据格式区别 +- 系统字典所有ID都是大于0的int64类型 +- 内置字典ID都是小于0的int64类型。枚举字典以20000开头,如:-200001381053496;方法字典以30000开头,如:-30000892528327;开头以外数字是根据数据选项的`key`值进行哈希算法得出 + ### 地理定位 ```go // 待写 diff --git a/docs/guide-zh-CN/sys-payment.md b/docs/guide-zh-CN/sys-payment.md index 7b13c0c..b13460f 100644 --- a/docs/guide-zh-CN/sys-payment.md +++ b/docs/guide-zh-CN/sys-payment.md @@ -55,7 +55,7 @@ func main() { ### 注册支付回调 -- 在文件`server/internal/global/pay.go` 加入你的业务订单分组回调方法,当订单支付成功验签通过后会自动进行回调,参考以下: +- 在文件`server/internal/logic/pay/notify.go` 加入你的业务订单分组回调方法,当订单支付成功验签通过后会自动进行回调,参考以下: ```go package global @@ -66,12 +66,13 @@ import ( "hotgo/internal/service" ) -// 注册支付成功回调方法 -func payNotifyCall() { - payment.RegisterNotifyCall(consts.OrderGroupAdminOrder, service.AdminOrder().PayNotify) // 后台充值订单 - // ... +// RegisterNotifyCall 注册支付成功回调方法 +func (s *sPay) RegisterNotifyCall() { + payment.RegisterNotifyCallMap(map[string]payment.NotifyCallFunc{ + consts.OrderGroupAdminOrder: service.AdminOrder().PayNotify, // 后台充值订单 + // ... + }) } - ``` ### 订单退款 diff --git a/docs/guide-zh-CN/sys-tcp-server.md b/docs/guide-zh-CN/sys-tcp-server.md index 8ef612f..81401d0 100644 --- a/docs/guide-zh-CN/sys-tcp-server.md +++ b/docs/guide-zh-CN/sys-tcp-server.md @@ -9,7 +9,7 @@ - 服务认证 - 更多 -> HotGo基于GF框架的TCP服务器组件,提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能,如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等,大大简化和规范了服务器开发流程。 +> HotGo基于GoFrame的TCP服务器组件,提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能,如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等,大大简化和规范了服务器开发流程。 ### 配置文件 - 配置文件:server/manifest/config/config.yaml diff --git a/docs/guide-zh-CN/sys-webhook.md b/docs/guide-zh-CN/sys-webhook.md index c8adf35..236d34f 100644 --- a/docs/guide-zh-CN/sys-webhook.md +++ b/docs/guide-zh-CN/sys-webhook.md @@ -1,3 +1,3 @@ ## WebHook -待写 +请参考:https://goframe.org/pages/viewpage.action?pageId=1114387 diff --git a/docs/guide-zh-CN/sys-websocket-client.md b/docs/guide-zh-CN/sys-websocket-client.md new file mode 100644 index 0000000..78b4617 --- /dev/null +++ b/docs/guide-zh-CN/sys-websocket-client.md @@ -0,0 +1,210 @@ +## WebSocket客户端 + +目录 + +- 全局消息监听 +- 单页面消息监听 +- 发送消息 + +> 基于WebSocket服务器,hotgo还对客户端的上做了一些封装,使其使用起来更加方便 +- [WebSocket服务器](sys-websocket-server.md) + +### 全局消息监听 +- 所有全局的消息监听都在这里 +- 文件路径:web/src/utils/websocket/registerMessage.ts +```ts +import { TABS_ROUTES } from '@/store/mutation-types'; +import { SocketEnum } from '@/enums/socketEnum'; +import { useUserStoreWidthOut } from '@/store/modules/user'; +import { notificationStoreWidthOut } from '@/store/modules/notification'; +import { addOnMessage, WebSocketMessage } from '@/utils/websocket/index'; + +// 注册全局消息监听 +export function registerGlobalMessage() { + // 心跳 + addOnMessage(SocketEnum.EventPing, function (_message: WebSocketMessage) { + // console.log('ping..'); + }); + + // 强制退出 + addOnMessage(SocketEnum.EventKick, function (_message: WebSocketMessage) { + const useUserStore = useUserStoreWidthOut(); + useUserStore.logout().then(() => { + // 移除标签页 + localStorage.removeItem(TABS_ROUTES); + location.reload(); + }); + }); + + // 消息通知 + addOnMessage(SocketEnum.EventNotice, function (message: WebSocketMessage) { + const notificationStore = notificationStoreWidthOut(); + notificationStore.triggerNewMessages(message.data); + }); + + // 更多全局消息处理都可以在这里注册 + // ... +} + +``` + +#### 单页面消息监听 +- 当你只需要某个页面使用WebSocket,这将是一个不错的选择,下面是一个简单的演示例子 +- 文件路径:web/src/views/addons/hgexample/portal/websocketTest.vue +```vue + + + + + +``` + +#### 发送消息 +- 向服务器发送一条消息 +```ts + import { sendMsg } from '@/utils/websocket'; + + const event = 'admin/addons/hgexample/testMessage'; // 消息路由 + const data: object | null = { // 消息内容 + message: 'message content...', + }; + const isRetry = false; // 发送失败是否重试,不传默认为true + + // 基本使用 + sendMsg(event, data); + + // 无消息内容 + sendMsg(event); + + // 发送失败不重试 + sendMsg(event, data, isRetry); +``` diff --git a/docs/guide-zh-CN/sys-websocket-server.md b/docs/guide-zh-CN/sys-websocket-server.md new file mode 100644 index 0000000..288e00c --- /dev/null +++ b/docs/guide-zh-CN/sys-websocket-server.md @@ -0,0 +1,143 @@ +## WebSocket服务器 + +目录 + +- 一个基本的消息收发例子 +- 常用方法 +- HTTP接口 +- 其他 + +> hotgo提供了一个WebSocket服务器,随`HTTP服务`启停。集成了许多常用功能,如JWT身份认证、路由消息处理器、一对一消息/群组消息/广播消息、在线用户管理、心跳保持等,大大简化和规范了WebSocket服务器的开发流程。 +- [Websocket客户端](sys-websocket-client.md) + +### 一个基本的消息收发例子 +- 这是一个基本的消息接收并进行处理的简单例子 + +#### 1.消息处理接口 +- 消息处理在设计上采用了接口化的思路。只需要实现以下接口,即可进行WebSocket消息注册 +- 文件路径:server/internal/websocket/model.go +```go +package websocket + +// EventHandler 消息处理器 +type EventHandler func(client *Client, req *WRequest) +``` + +#### 2.定义消息处理方法 +- 以下是功能案例中的一个简单演示,实现了消息处理接口,并将收到的消息原样发送给客户端 +- 文件路径:server/addons/hgexample/controller/websocket/handler/index.go +```go +package handler + +import ( + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "hotgo/internal/websocket" +) + +var ( + Index = cIndex{} +) + +type cIndex struct{} + +// TestMessage 测试消息 +func (c *cIndex) TestMessage(client *websocket.Client, req *websocket.WRequest) { + g.Log().Infof(client.Context(), "收到客户端测试消息:%v", gjson.New(req).String()) + // 将收到的消息原样发送给客户端 + websocket.SendSuccess(client, req.Event, req.Data) +} +``` + +#### 3.注册消息 +- 定义消息处理方法后,需要将其注册到WebSocket消息处理器,一般放在对应应用模块的`router/websocket.go`下即可 +- 文件路径:server/addons/hgexample/router/websocket.go +```go +package router + +import ( + "context" + "github.com/gogf/gf/v2/net/ghttp" + "hotgo/addons/hgexample/controller/websocket" + "hotgo/addons/hgexample/controller/websocket/handler" + ws "hotgo/internal/websocket" +) + +// WebSocket ws路由配置 +func WebSocket(ctx context.Context, group *ghttp.RouterGroup) { + // 注册消息路由 + ws.RegisterMsg(ws.EventHandlers{ + "admin/addons/hgexample/testMessage": handler.Index.TestMessage, // 测试消息 + }) + + // 这里"admin/addons/hgexample/testMessage"代表的是一个消息处理ID,可以自定义。建议的格式是和HTTP接口格式保持一致,这样还可以便于对用户请求的消息进行权限验证 + // 客户端连接后,向WebSocket服务器发送event为"admin/addons/hgexample/testMessage"的消息时,会调用TestMessage方法 +} +``` + +- 到此,你已了解了WebSocket消息接收并进行处理的基本流程 + + +### 常用方法 +- websocket服务器还提供了一些常用的方法,下面只对部分进行说明 +```go +func test() { + websocket.SendToAll() // 发送全部客户端 + websocket.SendToClientID() // 发送单个客户端 + websocket.SendToUser() // 发送单个用户 + websocket.SendToTag() // 发送某个标签、群组 + + client := websocket.Manager().GetClient(id) // 通过连接ID获取客户端连接 + client := websocket.Manager().GetUserClient(userId) // 通过用户ID获取客户端连接,因为用户是可多端登录的,这里返回的是一个切片 + + websocket.SendSuccess(client, "admin/addons/hgexample/testMessage", "消息内容") // 向指定客户端发送一条成功的消息 + websocket.SendError(client, "admin/addons/hgexample/testMessage", gerror.New("错误内容")) // 向指定客户端发送一条失败的消息 + +} +``` + + +### HTTP接口 +- 你还可以通过http接口方式调用WebSocket发送消息 +- 参考文件:server/internal/controller/websocket/send.go + + +### 其他 +- WebSocket被连接时需验证用户认证中间件,所以用户必须登录成功后才能连接成功 +- 参考文件:server/internal/logic/middleware/weboscket_auth.go +```go +package middleware + +import ( + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/text/gstr" + "hotgo/internal/consts" + "hotgo/internal/library/response" + "hotgo/utility/simple" +) + +// WebSocketAuth websocket鉴权中间件 +func (s *sMiddleware) WebSocketAuth(r *ghttp.Request) { + var ( + ctx = r.Context() + path = gstr.Replace(r.URL.Path, simple.RouterPrefix(ctx, consts.AppWebSocket), "", 1) + ) + + // 不需要验证登录的路由地址 + if s.IsExceptLogin(ctx, consts.AppWebSocket, path) { + r.Middleware.Next() + return + } + + // 将用户信息传递到上下文中 + if err := s.DeliverUserContext(r); err != nil { + response.JsonExit(r, gcode.CodeNotAuthorized.Code(), err.Error()) + return + } + + r.Middleware.Next() +} +``` + +- 如果您不要求用户进行登录即可使用 WebSocket,那么需要对身份验证中间件进行修改。然而,这样做会降低连接的安全性,并且无法应用于需要确定用户身份的情景,因此并不建议采取这种策略 diff --git a/docs/guide-zh-CN/web-form.md b/docs/guide-zh-CN/web-form.md index d23b812..17a8a19 100644 --- a/docs/guide-zh-CN/web-form.md +++ b/docs/guide-zh-CN/web-form.md @@ -21,6 +21,7 @@ - 单文件上传 UploadFile - 多文件上传 UploadFile - 文件选择器 FileChooser +- 大文件上传 MultipartUpload - 开关 Switch - 评分 Rate - 省市区选择器 CitySelector @@ -795,6 +796,33 @@ type FileType = 'image' | 'doc' | 'audio' | 'video' | 'zip' | 'other' | 'default ``` +### 大文件上传 MultipartUpload +- 基础用法 +```vue + + + +``` + ### 开关 Switch ```vue @@ -38,6 +38,8 @@ diff --git a/web/src/enums/socketEnum.ts b/web/src/enums/socketEnum.ts index 12894da..d3baddc 100644 --- a/web/src/enums/socketEnum.ts +++ b/web/src/enums/socketEnum.ts @@ -1,16 +1,11 @@ export enum SocketEnum { EventPing = 'ping', + EventKick = 'kick', + EventNotice = 'notice', EventConnected = 'connected', EventAdminMonitorTrends = 'admin/monitor/trends', EventAdminMonitorRunInfo = 'admin/monitor/runInfo', EventAdminOrderNotify = 'admin/order/notify', - TypeQueryUser = 2, - TypeBoardCastMsg = 3, - TypeQuerySwitcher = 4, - TypeQueryEndlessRank = 5, - TypeSendEmail = 6, - TypeQueryUserGuide = 7, - TypeRestartLog = 90, HeartBeatInterval = 1000, CodeSuc = 0, CodeErr = -1, diff --git a/web/src/hooks/useCreateScript.ts b/web/src/hooks/useCreateScript.ts new file mode 100644 index 0000000..a8c6632 --- /dev/null +++ b/web/src/hooks/useCreateScript.ts @@ -0,0 +1,21 @@ +import { onMounted } from 'vue'; + +export default function userCreateScript(src: string) { + const createScriptPromise = new Promise((resolve, reject) => { + onMounted(() => { + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.onload = () => { + resolve(''); + }; + script.onerror = (error) => { + reject(error); + }; + script.src = src; + document.head.appendChild(script); + }); + }); + return { + createScriptPromise, + }; +} diff --git a/web/src/layout/components/Footer/index.ts b/web/src/layout/components/Footer/index.ts deleted file mode 100644 index e94dce9..0000000 --- a/web/src/layout/components/Footer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import PageFooter from './index.vue'; - -export { PageFooter }; diff --git a/web/src/layout/components/Footer/index.vue b/web/src/layout/components/Footer/index.vue deleted file mode 100644 index 6202799..0000000 --- a/web/src/layout/components/Footer/index.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/web/src/layout/index.vue b/web/src/layout/index.vue index 5f96d5c..5e6d32f 100644 --- a/web/src/layout/index.vue +++ b/web/src/layout/index.vue @@ -58,10 +58,6 @@ - - - - diff --git a/web/src/main.ts b/web/src/main.ts index b082c7c..672dfbb 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -5,7 +5,7 @@ import router, { setupRouter } from './router'; import { setupStore } from '@/store'; import { setupNaive, setupDirectives } from '@/plugins'; import { AppProvider } from '@/components/Application'; -import Websocket from '@/utils/websocket'; +import setupWebsocket from '@/utils/websocket/index'; async function bootstrap() { const appProvider = createApp(AppProvider); @@ -36,15 +36,7 @@ async function bootstrap() { // 路由准备就绪后挂载APP实例 await router.isReady(); - // 全局websocket - const onMessageList: Array = []; - app.provide('onMessageList', onMessageList); - const onMessage = (event: any) => { - onMessageList.forEach((f) => { - f.call(null, event); - }); - }; - Websocket(onMessage); + setupWebsocket(); app.mount('#app', true); } diff --git a/web/src/utils/highHtml.ts b/web/src/utils/highHtml.ts new file mode 100644 index 0000000..d15c2cb --- /dev/null +++ b/web/src/utils/highHtml.ts @@ -0,0 +1,186 @@ +// 关键词配置 +interface IKeywordOption { + keyword: string | RegExp; + color?: string; + bgColor?: string; + style?: Record; + // 高亮标签名 + tagName?: string; + // 忽略大小写 + caseSensitive?: boolean; + // 自定义渲染高亮html + // eslint-disable-next-line no-unused-vars + renderHighlightKeyword?: (content: string) => any; +} + +type IKeyword = string | IKeywordOption; + +export interface IMatchIndex { + index: number; + subString: string; +} + +// 关键词索引 +export interface IKeywordParseIndex { + keyword: string | RegExp; + indexList: IMatchIndex[]; + option?: IKeywordOption; +} + +// 关键词 +export interface IKeywordParseResult { + start: number; + end: number; + subString?: string; + option?: IKeywordOption; +} + +// 计算 +const getKeywordIndexList = (content: string, keyword: string | RegExp, flags = 'ig') => { + const reg = new RegExp(keyword, flags); + const res = (content as any).matchAll(reg); + const arr = [...res]; + const allIndexArr: IMatchIndex[] = arr.map((e) => ({ + index: e.index, + subString: e['0'], + })); + return allIndexArr; +}; + +// 驼峰转换横线 +function humpToLine(name: string) { + return name.replace(/([A-Z])/g, '-$1').toLowerCase(); +} + +const renderNodeTag = (subStr: string, option: IKeywordOption) => { + const s = subStr; + if (!option) { + return s; + } + const { tagName = 'mark', bgColor, color, style = {}, renderHighlightKeyword } = option; + if (typeof renderHighlightKeyword === 'function') { + return renderHighlightKeyword(subStr); + } + style.backgroundColor = bgColor; + style.color = color; + + const styleContent = Object.keys(style) + .map((k) => `${humpToLine(k)}:${style[k]}`) + .join(';'); + const styleStr = `style="${styleContent}"`; + return `<${tagName} ${styleStr}>${s}`; +}; + +const renderHighlightHtml = (content: string, list: any[]) => { + let str = ''; + list.forEach((item) => { + const { start, end, option } = item; + const s = content.slice(start, end); + const subStr = renderNodeTag(s, option); + str += subStr; + item.subString = subStr; + }); + return str; +}; + +// 解析关键词为索引 +const parseHighlightIndex = (content: string, keywords: IKeyword[]) => { + const result: IKeywordParseIndex[] = []; + keywords.forEach((keywordOption: IKeyword) => { + let option: IKeywordOption = { keyword: '' }; + if (typeof keywordOption === 'string') { + option = { keyword: keywordOption }; + } else { + option = keywordOption; + } + const { keyword, caseSensitive = true } = option; + const indexList = getKeywordIndexList(content, keyword, caseSensitive ? 'g' : 'gi'); + const res = { + keyword, + indexList, + option, + }; + result.push(res); + }); + return result; +}; + +const parseHighlightString = (content: string, keywords: IKeyword[]) => { + const result = parseHighlightIndex(content, keywords); + const splitList: IKeywordParseResult[] = []; + const findSplitIndex = (index: number, len: number) => { + for (let i = 0; i < splitList.length; i++) { + const cur = splitList[i]; + // 有交集 + if ( + (index > cur.start && index < cur.end) || + (index + len > cur.start && index + len < cur.end) || + (cur.start > index && cur.start < index + len) || + (cur.end > index && cur.end < index + len) || + (index === cur.start && index + len === cur.end) + ) { + return -1; + } + // 没有交集,且在当前的前面 + if (index + len <= cur.start) { + return i; + } + // 没有交集,且在当前的后面的,放在下个迭代处理 + } + return splitList.length; + }; + result.forEach(({ indexList, option }: IKeywordParseIndex) => { + indexList.forEach((e) => { + const { index, subString } = e; + const item = { + start: index, + end: index + subString.length, + option, + }; + const splitIndex = findSplitIndex(index, subString.length); + if (splitIndex !== -1) { + splitList.splice(splitIndex, 0, item); + } + }); + }); + + // 补上没有匹配关键词的部分 + const list: IKeywordParseResult[] = []; + splitList.forEach((cur, i) => { + const { start, end } = cur; + const next = splitList[i + 1]; + // 第一个前面补一个 + if (i === 0 && start > 0) { + list.push({ start: 0, end: start, subString: content.slice(0, start) }); + } + list.push({ ...cur, subString: content.slice(start, end) }); + // 当前和下一个中间补一个 + if (next?.start > end) { + list.push({ + start: end, + end: next.start, + subString: content.slice(end, next.start), + }); + } + // 最后一个后面补一个 + if (i === splitList.length - 1 && end < content.length - 1) { + list.push({ + start: end, + end: content.length - 1, + subString: content.slice(end, content.length), + }); + } + }); + return list; +}; + +// 生成关键词高亮的html字符串 +const highHtml = (content: string, keywords: IKeyword[]) => { + const splitList = parseHighlightString(content, keywords); + return { + highText: renderHighlightHtml(content, splitList), + highList: splitList, + }; +}; + +export default highHtml; diff --git a/web/src/utils/hotgo.ts b/web/src/utils/hotgo.ts index f2b66f9..7e3ddce 100644 --- a/web/src/utils/hotgo.ts +++ b/web/src/utils/hotgo.ts @@ -84,3 +84,17 @@ export function timeFix() { ? '下午好' : '晚上好'; } + +// 随机浅色 +export function rdmLightRgbColor(): string { + const letters = '456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + if (i === 0) { + color += 'F'; // 确保第一个字符较亮 + } else { + color += letters[Math.floor(Math.random() * letters.length)]; + } + } + return color; +} diff --git a/web/src/utils/http/axios/Axios.ts b/web/src/utils/http/axios/Axios.ts index 531247a..f8580bd 100644 --- a/web/src/utils/http/axios/Axios.ts +++ b/web/src/utils/http/axios/Axios.ts @@ -1,10 +1,11 @@ import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios'; import axios from 'axios'; import { AxiosCanceler } from './axiosCancel'; -import { isFunction } from '@/utils/is'; +import { isFunction, isString, isUrl } from '@/utils/is'; import { cloneDeep } from 'lodash-es'; import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types'; import { ContentTypeEnum } from '@/enums/httpEnum'; +import { useGlobSetting } from '@/hooks/setting'; export * from './axiosTransform'; @@ -107,19 +108,29 @@ export class VAxios { /** * @description: 文件上传 */ - uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { - const formData = new window.FormData(); - const customFilename = params.name || 'file'; + uploadFile(config: AxiosRequestConfig, params: UploadFileParams, options?: RequestOptions) { + const transform = this.getTransform(); + const { requestCatch, transformRequestData } = transform || {}; + const { requestOptions } = this.options; + const opt: RequestOptions = Object.assign({}, requestOptions, options); - if (params.filename) { - formData.append(customFilename, params.file, params.filename); - } else { - formData.append(customFilename, params.file); + const globSetting = useGlobSetting(); + const urlPrefix = globSetting.urlPrefix || ''; + const apiUrl = globSetting.apiUrl || ''; + const isUrlStr = isUrl(config.url as string); + + if (!isUrlStr) { + config.url = `${urlPrefix}${config.url}`; } - if (params.data) { - Object.keys(params.data).forEach((key) => { - const value = params.data![key]; + if (!isUrlStr && apiUrl && isString(apiUrl)) { + config.url = `${apiUrl}${config.url}`; + } + + const formData = new window.FormData(); + if (params) { + Object.keys(params).forEach((key) => { + const value = params![key]; if (Array.isArray(value)) { value.forEach((item) => { formData.append(`${key}[]`, item); @@ -127,18 +138,42 @@ export class VAxios { return; } - formData.append(key, params.data![key]); + formData.append(key, params![key]); }); } - return this.axiosInstance.request({ - method: 'POST', - data: formData, - headers: { - 'Content-type': ContentTypeEnum.FORM_DATA, - ignoreCancelToken: true, - }, - ...config, + return new Promise((resolve, reject) => { + this.axiosInstance + .request({ + method: 'POST', + data: formData, + headers: { + 'Content-type': ContentTypeEnum.FORM_DATA, + ignoreCancelToken: true, + }, + ...config, + }) + .then((res: AxiosResponse) => { + // 请求是否被取消 + const isCancel = axios.isCancel(res); + if (transformRequestData && isFunction(transformRequestData) && !isCancel) { + try { + const ret = transformRequestData(res, opt); + resolve(ret); + } catch (err) { + reject(err || new Error('request error!')); + } + return; + } + resolve(res as unknown as Promise); + }) + .catch((e: Error) => { + if (requestCatch && isFunction(requestCatch)) { + reject(requestCatch(e)); + return; + } + reject(e); + }); }); } diff --git a/web/src/utils/websocket.ts b/web/src/utils/websocket/index.ts similarity index 50% rename from web/src/utils/websocket.ts rename to web/src/utils/websocket/index.ts index 5780131..49ef48e 100644 --- a/web/src/utils/websocket.ts +++ b/web/src/utils/websocket/index.ts @@ -1,69 +1,21 @@ import { SocketEnum } from '@/enums/socketEnum'; -import { notificationStoreWidthOut } from '@/store/modules/notification'; import { useUserStoreWidthOut } from '@/store/modules/user'; -import { TABS_ROUTES } from '@/store/mutation-types'; import { isJsonString } from '@/utils/is'; +import { registerGlobalMessage } from '@/utils/websocket/registerMessage'; + +// WebSocket消息格式 +export interface WebSocketMessage { + event: string; + data: any; + code: number; + timestamp: number; +} let socket: WebSocket; let isActive: boolean; +const messageHandler: Map = new Map(); -export function getSocket(): WebSocket { - if (socket === undefined) { - location.reload(); - } - return socket; -} - -export function getActive(): boolean { - return isActive; -} - -export function sendMsg(event: string, data = null, isRetry = true) { - if (socket === undefined || !isActive) { - if (!isRetry) { - console.log('socket连接异常,发送失败!'); - return; - } - console.log('socket连接异常,等待重试..'); - setTimeout(function () { - sendMsg(event, data); - }, 200); - return; - } - - try { - socket.send( - JSON.stringify({ - event: event, - data: data, - }) - ); - } catch (err) { - // @ts-ignore - console.log('ws发送消息失败,等待重试,err:' + err.message); - if (!isRetry) { - return; - } - setTimeout(function () { - sendMsg(event, data); - }, 100); - } -} - -export function addOnMessage(onMessageList: any, func: Function) { - let exist = false; - for (let i = 0; i < onMessageList.length; i++) { - if (onMessageList[i].name == func.name) { - onMessageList[i] = func; - exist = true; - } - } - if (!exist) { - onMessageList.push(func); - } -} - -export default (onMessage: Function) => { +export default () => { const heartCheck = { timeout: 5000, timeoutObj: setTimeout(() => {}), @@ -85,35 +37,37 @@ export default (onMessage: Function) => { }) ); self.serverTimeoutObj = setTimeout(function () { - console.log('关闭服务'); + console.log('[WebSocket] 关闭服务'); socket.close(); }, self.timeout); }, this.timeout); }, }; - const notificationStore = notificationStoreWidthOut(); const useUserStore = useUserStoreWidthOut(); let lockReconnect = false; let timer: ReturnType; const createSocket = () => { - console.log('createSocket...'); + console.log('[WebSocket] createSocket...'); + if (useUserStore.token === '') { + console.error('[WebSocket] 用户未登录,稍后重试...'); + reconnect(); + return; + } try { - if (useUserStore.token === '') { - throw new Error('用户未登录,稍后重试...'); - } - socket = new WebSocket(useUserStore.config?.wsAddr + '?authorization=' + useUserStore.token); + socket = new WebSocket(`${useUserStore.config?.wsAddr}?authorization=${useUserStore.token}`); init(); } catch (e) { - console.log('createSocket err:' + e); + console.error(`[WebSocket] createSocket err: ${e}`); reconnect(); } if (lockReconnect) { lockReconnect = false; } }; + const reconnect = () => { - console.log('lockReconnect:' + lockReconnect); + console.log('[WebSocket] lockReconnect:' + lockReconnect); if (lockReconnect) return; lockReconnect = true; clearTimeout(timer); @@ -124,7 +78,7 @@ export default (onMessage: Function) => { const init = () => { socket.onopen = function (_) { - console.log('WebSocket:已连接'); + console.log('[WebSocket] 已连接'); heartCheck.reset().start(); isActive = true; }; @@ -133,47 +87,25 @@ export default (onMessage: Function) => { isActive = true; // console.log('WebSocket:收到一条消息', event.data); - let isHeart = false; if (!isJsonString(event.data)) { - console.log('socket message incorrect format:' + JSON.stringify(event)); + console.log('[WebSocket] message incorrect format:' + JSON.stringify(event)); return; } - const message = JSON.parse(event.data); - if (message.event === 'ping') { - isHeart = true; - } - - // 强制退出 - if (message.event === 'kick') { - useUserStore.logout().then(() => { - // 移除标签页 - localStorage.removeItem(TABS_ROUTES); - location.reload(); - }); - return; - } - - // 通知 - if (message.event === 'notice') { - notificationStore.triggerNewMessages(message.data); - return; - } - - if (onMessage && !isHeart) { - onMessage.call(null, event); - } heartCheck.reset().start(); + + const message = JSON.parse(event.data) as WebSocketMessage; + onMessage(message); }; socket.onerror = function (_) { - console.log('WebSocket:发生错误'); + console.log('[WebSocket] 发生错误'); reconnect(); isActive = false; }; socket.onclose = function (_) { - console.log('WebSocket:已关闭'); + console.log('[WebSocket] 已关闭'); heartCheck.reset(); reconnect(); isActive = false; @@ -186,4 +118,63 @@ export default (onMessage: Function) => { }; createSocket(); + registerGlobalMessage(); }; + +function onMessage(message: WebSocketMessage) { + let handled = false; + messageHandler.forEach((value: Function, key: string) => { + if (message.event === key || key === '*') { + handled = true; + value.call(null, message); + } + }); + + if (!handled) { + console.log('[WebSocket] messageHandler not registered. message:' + JSON.stringify(message)); + } +} + +// 发送消息 +export function sendMsg(event: string, data: any = null, isRetry = true) { + if (socket === undefined || !isActive) { + if (!isRetry) { + console.log('[WebSocket] 连接异常,发送失败!'); + return; + } + console.log('[WebSocket] 连接异常,等待重试..'); + setTimeout(() => { + sendMsg(event, data); + }, 200); + return; + } + + try { + socket.send(JSON.stringify({ event, data })); + } catch (err: any) { + console.log('[WebSocket] 发送消息失败,err:', err.message); + if (!isRetry) { + return; + } + + console.log('[WebSocket] 等待重试..'); + setTimeout(() => { + sendMsg(event, data); + }, 100); + } +} + +// 添加消息处理 +export function addOnMessage(key: string, value: Function): void { + messageHandler.set(key, value); +} + +// 移除消息处理 +export function removeOnMessage(key: string): boolean { + return messageHandler.delete(key); +} + +// 查看所有消息处理 +export function getAllOnMessage(): Map { + return messageHandler; +} diff --git a/web/src/utils/websocket/registerMessage.ts b/web/src/utils/websocket/registerMessage.ts new file mode 100644 index 0000000..0a9d27c --- /dev/null +++ b/web/src/utils/websocket/registerMessage.ts @@ -0,0 +1,32 @@ +import { TABS_ROUTES } from '@/store/mutation-types'; +import { SocketEnum } from '@/enums/socketEnum'; +import { useUserStoreWidthOut } from '@/store/modules/user'; +import { notificationStoreWidthOut } from '@/store/modules/notification'; +import { addOnMessage, WebSocketMessage } from '@/utils/websocket/index'; + +// 注册全局消息监听 +export function registerGlobalMessage() { + // 心跳 + addOnMessage(SocketEnum.EventPing, function (_message: WebSocketMessage) { + // console.log('ping..'); + }); + + // 强制退出 + addOnMessage(SocketEnum.EventKick, function (_message: WebSocketMessage) { + const useUserStore = useUserStoreWidthOut(); + useUserStore.logout().then(() => { + // 移除标签页 + localStorage.removeItem(TABS_ROUTES); + location.reload(); + }); + }); + + // 消息通知 + addOnMessage(SocketEnum.EventNotice, function (message: WebSocketMessage) { + const notificationStore = notificationStoreWidthOut(); + notificationStore.triggerNewMessages(message.data); + }); + + // 更多全局消息处理都可以在这里注册 + // ... +} diff --git a/web/src/views/addons/hgexample/comp/calendar/index.vue b/web/src/views/addons/hgexample/comp/calendar/index.vue new file mode 100644 index 0000000..f3cba1b --- /dev/null +++ b/web/src/views/addons/hgexample/comp/calendar/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/web/src/views/addons/hgexample/comp/des/index.vue b/web/src/views/addons/hgexample/comp/des/index.vue new file mode 100644 index 0000000..2031fca --- /dev/null +++ b/web/src/views/addons/hgexample/comp/des/index.vue @@ -0,0 +1,89 @@ + + diff --git a/web/src/views/addons/hgexample/comp/directive/index.vue b/web/src/views/addons/hgexample/comp/directive/index.vue new file mode 100644 index 0000000..5f264d8 --- /dev/null +++ b/web/src/views/addons/hgexample/comp/directive/index.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/drag/index.vue b/web/src/views/addons/hgexample/comp/drag/index.vue new file mode 100644 index 0000000..c113ebe --- /dev/null +++ b/web/src/views/addons/hgexample/comp/drag/index.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/fingerprintjs/index.vue b/web/src/views/addons/hgexample/comp/fingerprintjs/index.vue new file mode 100644 index 0000000..081e497 --- /dev/null +++ b/web/src/views/addons/hgexample/comp/fingerprintjs/index.vue @@ -0,0 +1,38 @@ + + diff --git a/web/src/views/addons/hgexample/comp/form/basic.vue b/web/src/views/addons/hgexample/comp/form/basic.vue new file mode 100644 index 0000000..3236daf --- /dev/null +++ b/web/src/views/addons/hgexample/comp/form/basic.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/form/useForm.vue b/web/src/views/addons/hgexample/comp/form/useForm.vue new file mode 100644 index 0000000..9d8af3f --- /dev/null +++ b/web/src/views/addons/hgexample/comp/form/useForm.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/icons/antd.vue b/web/src/views/addons/hgexample/comp/icons/antd.vue new file mode 100644 index 0000000..4f63bfd --- /dev/null +++ b/web/src/views/addons/hgexample/comp/icons/antd.vue @@ -0,0 +1,23 @@ + + + + diff --git a/web/src/views/addons/hgexample/comp/icons/icons.vue b/web/src/views/addons/hgexample/comp/icons/icons.vue new file mode 100644 index 0000000..c9ee2dd --- /dev/null +++ b/web/src/views/addons/hgexample/comp/icons/icons.vue @@ -0,0 +1,136 @@ + + + + diff --git a/web/src/views/addons/hgexample/comp/icons/ionicons5.vue b/web/src/views/addons/hgexample/comp/icons/ionicons5.vue new file mode 100644 index 0000000..5387e24 --- /dev/null +++ b/web/src/views/addons/hgexample/comp/icons/ionicons5.vue @@ -0,0 +1,23 @@ + + + + diff --git a/web/src/views/addons/hgexample/comp/icons/selector.vue b/web/src/views/addons/hgexample/comp/icons/selector.vue new file mode 100644 index 0000000..9d5a33e --- /dev/null +++ b/web/src/views/addons/hgexample/comp/icons/selector.vue @@ -0,0 +1,29 @@ + + + + diff --git a/web/src/views/addons/hgexample/comp/import/excel.vue b/web/src/views/addons/hgexample/comp/import/excel.vue new file mode 100644 index 0000000..0d637fb --- /dev/null +++ b/web/src/views/addons/hgexample/comp/import/excel.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/index.vue b/web/src/views/addons/hgexample/comp/index.vue new file mode 100644 index 0000000..b380813 --- /dev/null +++ b/web/src/views/addons/hgexample/comp/index.vue @@ -0,0 +1,369 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/map/baidu.vue b/web/src/views/addons/hgexample/comp/map/baidu.vue new file mode 100644 index 0000000..92d81e5 --- /dev/null +++ b/web/src/views/addons/hgexample/comp/map/baidu.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/map/gaode.vue b/web/src/views/addons/hgexample/comp/map/gaode.vue new file mode 100644 index 0000000..8fc3943 --- /dev/null +++ b/web/src/views/addons/hgexample/comp/map/gaode.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/modal/index.vue b/web/src/views/addons/hgexample/comp/modal/index.vue new file mode 100644 index 0000000..461ca0a --- /dev/null +++ b/web/src/views/addons/hgexample/comp/modal/index.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/web/src/views/addons/hgexample/comp/moreComponents/index.vue b/web/src/views/addons/hgexample/comp/moreComponents/index.vue new file mode 100644 index 0000000..1aa5bfb --- /dev/null +++ b/web/src/views/addons/hgexample/comp/moreComponents/index.vue @@ -0,0 +1,15 @@ +