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 0000000..421973f Binary files /dev/null and b/docs/guide-zh-CN/images/sys-library-dict.png differ diff --git a/docs/guide-zh-CN/start-environment.md b/docs/guide-zh-CN/start-environment.md index fa40501..275150d 100644 --- a/docs/guide-zh-CN/start-environment.md +++ b/docs/guide-zh-CN/start-environment.md @@ -26,6 +26,6 @@ > 需要本地具有 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/components/Upload/src/BasicUpload.vue b/web/src/components/Upload/src/BasicUpload.vue index 35f8f14..2936ad5 100644 --- a/web/src/components/Upload/src/BasicUpload.vue +++ b/web/src/components/Upload/src/BasicUpload.vue @@ -106,6 +106,9 @@ const message = useMessage(); const dialog = useDialog(); const uploadTitle = ref(props.fileType === 'image' ? '上传图片' : '上传附件'); + const maxNumber = computed(() => { + return props.maxNumber; + }); const fileAvatarCSS = computed(() => { return { '--n-merged-size': `var(--n-avatar-size-override, ${props.width * 0.8}px)`, @@ -245,7 +248,7 @@ //上传结束 function finish({ event: Event }) { - const res = eval('(' + Event.target.response + ')'); + const res = JSON.parse(Event.target.response); const infoField = componentSetting.upload.apiSetting.infoField; const imgField = componentSetting.upload.apiSetting.imgField; const { code } = res; @@ -289,6 +292,7 @@ getFileExt, Preview, previewRef, + maxNumber, }; }, }); 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 @@ +