feat: add IM admin P2 configuration modules

This commit is contained in:
richjohnrambo
2026-08-22 00:36:38 +08:00
parent 5c3f6ae58d
commit d53b0c1b62
49 changed files with 3574 additions and 10 deletions
+94 -10
View File
@@ -1,6 +1,6 @@
# IM 未实现增强项 # IM 未实现增强项
> 更新时间:2026-08-21 > 更新时间:2026-08-22
> 范围:HotGo IM 后台、IM 服务端配合项、客户端动态配置来源 > 范围:HotGo IM 后台、IM 服务端配合项、客户端动态配置来源
本文用于记录当前后台已识别但尚未完成的增强项,作为后续开发排期和验收依据。当前多租户方案为“一个 HotGo 控制库 + 每租户一个独立 IM 业务库”,租户业务表不带 `tenant_id`,后台必须通过登录态解析租户并选择租户库。 本文用于记录当前后台已识别但尚未完成的增强项,作为后续开发排期和验收依据。当前多租户方案为“一个 HotGo 控制库 + 每租户一个独立 IM 业务库”,租户业务表不带 `tenant_id`,后台必须通过登录态解析租户并选择租户库。
@@ -254,15 +254,99 @@
## 后续 P2 ## 后续 P2
1. 工作台模块配置。 ### P2-1 工作台模块配置
2. 会议。
3. 日报/月报。 状态:已完成基础后台闭环。
4. 总经理信箱。
5. App 版本更新策略。 已实现:
6. 青少年模式。
7. 反诈与内容审核策略。 1. 租户工作台模块列表、搜索。
8. 举报/反馈原因配置、SLA、合并、导出。 2. 模块新增、编辑、启用/禁用。
9. 实名审核策略配置。 3. 模块排序接口和后台入口。
4. 默认初始化组织架构、会议通知、日报、月报、总经理信箱五个入口。
5. 路由选项对齐 IM 服务端当前白名单,避免配置后客户端接口丢弃。
6. 工作台配置变更写入 IM 后台审计。
待 IM 服务端/客户端配合确认:
1. `GET /v1/im/workbench` 当前读取 `im_workbench_apps` 已具备,需在实际环境确认租户库连接与后台修改后的缓存刷新策略。
2. 用户或角色级工作台权限表 `im_workbench_user_apps` 尚未接入后台,当前只覆盖租户级模块配置。
3. 未读角标实时事件 `workbench.badge.updated` 的聚合和增量推送仍由 IM 服务端承接。
4. 新增自定义入口若超出当前五个路由,需要先扩展 IM 服务端路由白名单和客户端页面。
### P2-2 App 版本更新策略
状态:已完成基础后台闭环。
已实现:
1. 租户级客户端版本策略列表、搜索。
2. 按 `platform + channel` 配置最新版本、最低支持版本、更新类型、商店地址、安装包地址、校验值和再次提醒时间。
3. 默认初始化 iOS/Android stable 策略。
4. 配置变更写入 IM 后台审计。
待 IM 服务端/客户端配合确认:
1. `GET /v1/im/config/app-version` 已有 IM 服务端读取链路,需确认实际环境租户库数据和客户端版本比较规则。
2. 灰度比例、分渠道灰度名单、强更弹窗样式尚未建模。
### P2-3 举报原因配置
状态:已完成基础后台闭环。
已实现:
1. 按用户、群组、消息三类对象配置举报原因。
2. 支持原因新增、编辑、禁用、排序、是否要求描述、是否允许上传证据。
3. 默认初始化诈骗、骚扰、垃圾广告、违法违规等基础原因。
4. 配置变更写入 IM 后台审计。
待实现/配合:
1. 举报工单 SLA、合并策略、批量导出仍未实现。
2. 举报工单处置结果与客户端通知联动仍由后续工单模块增强。
### P2-4 总经理信箱配置
状态:已完成基础后台闭环。
已实现:
1. 租户级总经理信箱启用/禁用。
2. 匿名提交开关。
3. 必填字段、最大字数、附件策略配置。
4. 告知标题和告知内容配置,更新时同步刷新 `notice_updated_at`。
5. 配置变更写入 IM 后台审计。
待实现/配合:
1. 信箱提交列表、处理状态、回复闭环尚未实现。
2. 受理人/部门、SLA、匿名身份展示范围尚未建模。
### P2-5 意见反馈配置
状态:已完成基础后台闭环。
已实现:
1. 租户级意见反馈分类配置。
2. 最大字数、附件策略、联系方式开关、诊断信息提示配置。
3. 配置变更写入 IM 后台审计。
待实现/配合:
1. 反馈提交列表、合并、回复、导出尚未实现。
2. 客户端分类文案如需多语言,需要扩展分类结构。
### P2 待实现
1. 会议。
2. 日报/月报。
3. 总经理信箱提交处理闭环。
4. 青少年模式。
5. 反诈与内容审核策略。
6. 举报/反馈 SLA、合并、导出。
7. 实名审核策略配置。
## 验收提示 ## 验收提示
@@ -0,0 +1,32 @@
package imabusereason
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imAbuseReason/list" method:"get" tags:"IM举报原因" summary:"获取IM举报原因列表"`
sysin.ImAbuseReasonListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImAbuseReasonModel `json:"list" dc:"数据列表"`
}
type EditReq struct {
g.Meta `path:"/imAbuseReason/edit" method:"post" tags:"IM举报原因" summary:"新增/编辑IM举报原因"`
sysin.ImAbuseReasonEditInp
}
type EditRes struct{}
type DeleteReq struct {
g.Meta `path:"/imAbuseReason/delete" method:"post" tags:"IM举报原因" summary:"禁用IM举报原因"`
sysin.ImAbuseReasonDeleteInp
}
type DeleteRes struct{}
@@ -0,0 +1,25 @@
package imapprelease
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imAppRelease/list" method:"get" tags:"IM版本更新" summary:"获取IM客户端版本策略列表"`
sysin.ImAppReleaseListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImAppReleaseModel `json:"list" dc:"数据列表"`
}
type EditReq struct {
g.Meta `path:"/imAppRelease/edit" method:"post" tags:"IM版本更新" summary:"新增/编辑IM客户端版本策略"`
sysin.ImAppReleaseEditInp
}
type EditRes struct{}
@@ -0,0 +1,23 @@
package imfeedbackconfig
import (
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type GetReq struct {
g.Meta `path:"/imFeedbackConfig/get" method:"get" tags:"IM意见反馈配置" summary:"获取IM意见反馈配置"`
sysin.ImFeedbackConfigGetInp
}
type GetRes struct {
*sysin.ImFeedbackConfigModel
}
type UpdateReq struct {
g.Meta `path:"/imFeedbackConfig/update" method:"post" tags:"IM意见反馈配置" summary:"更新IM意见反馈配置"`
sysin.ImFeedbackConfigUpdateInp
}
type UpdateRes struct{}
@@ -0,0 +1,23 @@
package immailboxconfig
import (
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type GetReq struct {
g.Meta `path:"/imMailboxConfig/get" method:"get" tags:"IM总经理信箱配置" summary:"获取IM总经理信箱配置"`
sysin.ImMailboxConfigGetInp
}
type GetRes struct {
*sysin.ImMailboxConfigModel
}
type UpdateReq struct {
g.Meta `path:"/imMailboxConfig/update" method:"post" tags:"IM总经理信箱配置" summary:"更新IM总经理信箱配置"`
sysin.ImMailboxConfigUpdateInp
}
type UpdateRes struct{}
@@ -0,0 +1,39 @@
package imworkbench
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imWorkbench/list" method:"get" tags:"IM工作台" summary:"获取IM工作台模块列表"`
sysin.ImWorkbenchAppListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImWorkbenchAppModel `json:"list" dc:"数据列表"`
}
type EditReq struct {
g.Meta `path:"/imWorkbench/edit" method:"post" tags:"IM工作台" summary:"新增/编辑IM工作台模块"`
sysin.ImWorkbenchAppEditInp
}
type EditRes struct{}
type DeleteReq struct {
g.Meta `path:"/imWorkbench/delete" method:"post" tags:"IM工作台" summary:"禁用IM工作台模块"`
sysin.ImWorkbenchAppDeleteInp
}
type DeleteRes struct{}
type ReorderReq struct {
g.Meta `path:"/imWorkbench/reorder" method:"post" tags:"IM工作台" summary:"调整IM工作台模块排序"`
sysin.ImWorkbenchAppReorderInp
}
type ReorderRes struct{}
@@ -0,0 +1,52 @@
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImAppPlatformOptions", "IM客户端平台选项", ImAppPlatformOptions)
dict.RegisterEnums("ImAppUpdateTypeOptions", "IM客户端更新类型选项", ImAppUpdateTypeOptions)
dict.RegisterEnums("ImAbuseTargetTypeOptions", "IM举报对象类型选项", ImAbuseTargetTypeOptions)
dict.RegisterEnums("ImAbuseReasonEnabledOptions", "IM举报原因启用状态", ImAbuseReasonEnabledOptions)
}
const (
ImAppPlatformIos = "ios"
ImAppPlatformAndroid = "android"
)
const (
ImAppUpdateTypeNone = "none"
ImAppUpdateTypeRecommended = "recommended"
ImAppUpdateTypeForce = "force"
)
const (
ImAbuseTargetTypeUser = "user"
ImAbuseTargetTypeGroup = "group"
ImAbuseTargetTypeMessage = "message"
)
var ImAppPlatformOptions = []*model.Option{
dict.GenPrimaryOption(ImAppPlatformIos, "iOS"),
dict.GenSuccessOption(ImAppPlatformAndroid, "Android"),
}
var ImAppUpdateTypeOptions = []*model.Option{
dict.GenDefaultOption(ImAppUpdateTypeNone, "无需更新"),
dict.GenWarningOption(ImAppUpdateTypeRecommended, "建议更新"),
dict.GenErrorOption(ImAppUpdateTypeForce, "强制更新"),
}
var ImAbuseTargetTypeOptions = []*model.Option{
dict.GenPrimaryOption(ImAbuseTargetTypeUser, "用户"),
dict.GenSuccessOption(ImAbuseTargetTypeGroup, "群组"),
dict.GenWarningOption(ImAbuseTargetTypeMessage, "消息"),
}
var ImAbuseReasonEnabledOptions = []*model.Option{
dict.GenWarningOption(0, "禁用"),
dict.GenSuccessOption(1, "启用"),
}
+50
View File
@@ -0,0 +1,50 @@
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImWorkbenchAppEnabledOptions", "IM工作台模块启用状态", ImWorkbenchAppEnabledOptions)
dict.RegisterEnums("ImWorkbenchPermissionOptions", "IM工作台模块权限策略", ImWorkbenchPermissionOptions)
dict.RegisterEnums("ImWorkbenchRouteOptions", "IM工作台模块路由选项", ImWorkbenchRouteOptions)
}
const (
ImWorkbenchAppDisabled = 0
ImWorkbenchAppEnabled = 1
)
const (
ImWorkbenchPermissionEnabled = "enabled"
ImWorkbenchPermissionRoleRequired = "role_required"
ImWorkbenchPermissionDisabled = "disabled"
)
const (
ImWorkbenchRouteOrganization = "/organization"
ImWorkbenchRouteMeetings = "/meetings"
ImWorkbenchRouteDailyReports = "/reports/daily"
ImWorkbenchRouteMonthlyReports = "/reports/monthly"
ImWorkbenchRouteExecutiveMailbox = "/executive-mailbox"
)
var ImWorkbenchAppEnabledOptions = []*model.Option{
dict.GenWarningOption(ImWorkbenchAppDisabled, "禁用"),
dict.GenSuccessOption(ImWorkbenchAppEnabled, "启用"),
}
var ImWorkbenchPermissionOptions = []*model.Option{
dict.GenSuccessOption(ImWorkbenchPermissionEnabled, "全部可见"),
dict.GenPrimaryOption(ImWorkbenchPermissionRoleRequired, "按角色权限"),
dict.GenWarningOption(ImWorkbenchPermissionDisabled, "不可见"),
}
var ImWorkbenchRouteOptions = []*model.Option{
dict.GenPrimaryOption(ImWorkbenchRouteOrganization, "组织架构"),
dict.GenPrimaryOption(ImWorkbenchRouteMeetings, "会议通知"),
dict.GenPrimaryOption(ImWorkbenchRouteDailyReports, "日报"),
dict.GenPrimaryOption(ImWorkbenchRouteMonthlyReports, "月报"),
dict.GenPrimaryOption(ImWorkbenchRouteExecutiveMailbox, "总经理信箱"),
}
@@ -0,0 +1,36 @@
package sys
import (
"context"
"hotgo/api/admin/imabusereason"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImAbuseReason = cImAbuseReason{}
type cImAbuseReason struct{}
func (c *cImAbuseReason) List(ctx context.Context, req *imabusereason.ListReq) (res *imabusereason.ListRes, err error) {
list, totalCount, err := service.SysImAbuseReason().List(ctx, &req.ImAbuseReasonListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImAbuseReasonModel{}
}
res = new(imabusereason.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImAbuseReason) Edit(ctx context.Context, req *imabusereason.EditReq) (res *imabusereason.EditRes, err error) {
err = service.SysImAbuseReason().Edit(ctx, &req.ImAbuseReasonEditInp)
return
}
func (c *cImAbuseReason) Delete(ctx context.Context, req *imabusereason.DeleteReq) (res *imabusereason.DeleteRes, err error) {
err = service.SysImAbuseReason().Delete(ctx, &req.ImAbuseReasonDeleteInp)
return
}
@@ -0,0 +1,31 @@
package sys
import (
"context"
"hotgo/api/admin/imapprelease"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImAppRelease = cImAppRelease{}
type cImAppRelease struct{}
func (c *cImAppRelease) List(ctx context.Context, req *imapprelease.ListReq) (res *imapprelease.ListRes, err error) {
list, totalCount, err := service.SysImAppRelease().List(ctx, &req.ImAppReleaseListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImAppReleaseModel{}
}
res = new(imapprelease.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImAppRelease) Edit(ctx context.Context, req *imapprelease.EditReq) (res *imapprelease.EditRes, err error) {
err = service.SysImAppRelease().Edit(ctx, &req.ImAppReleaseEditInp)
return
}
@@ -0,0 +1,25 @@
package sys
import (
"context"
"hotgo/api/admin/imfeedbackconfig"
"hotgo/internal/service"
)
var ImFeedbackConfig = cImFeedbackConfig{}
type cImFeedbackConfig struct{}
func (c *cImFeedbackConfig) Get(ctx context.Context, req *imfeedbackconfig.GetReq) (res *imfeedbackconfig.GetRes, err error) {
data, err := service.SysImFeedbackConfig().Get(ctx, &req.ImFeedbackConfigGetInp)
if err != nil {
return
}
res = &imfeedbackconfig.GetRes{ImFeedbackConfigModel: data}
return
}
func (c *cImFeedbackConfig) Update(ctx context.Context, req *imfeedbackconfig.UpdateReq) (res *imfeedbackconfig.UpdateRes, err error) {
err = service.SysImFeedbackConfig().Update(ctx, &req.ImFeedbackConfigUpdateInp)
return
}
@@ -0,0 +1,25 @@
package sys
import (
"context"
"hotgo/api/admin/immailboxconfig"
"hotgo/internal/service"
)
var ImMailboxConfig = cImMailboxConfig{}
type cImMailboxConfig struct{}
func (c *cImMailboxConfig) Get(ctx context.Context, req *immailboxconfig.GetReq) (res *immailboxconfig.GetRes, err error) {
data, err := service.SysImMailboxConfig().Get(ctx, &req.ImMailboxConfigGetInp)
if err != nil {
return
}
res = &immailboxconfig.GetRes{ImMailboxConfigModel: data}
return
}
func (c *cImMailboxConfig) Update(ctx context.Context, req *immailboxconfig.UpdateReq) (res *immailboxconfig.UpdateRes, err error) {
err = service.SysImMailboxConfig().Update(ctx, &req.ImMailboxConfigUpdateInp)
return
}
@@ -0,0 +1,41 @@
package sys
import (
"context"
"hotgo/api/admin/imworkbench"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImWorkbench = cImWorkbench{}
type cImWorkbench struct{}
func (c *cImWorkbench) List(ctx context.Context, req *imworkbench.ListReq) (res *imworkbench.ListRes, err error) {
list, totalCount, err := service.SysImWorkbench().List(ctx, &req.ImWorkbenchAppListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImWorkbenchAppModel{}
}
res = new(imworkbench.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImWorkbench) Edit(ctx context.Context, req *imworkbench.EditReq) (res *imworkbench.EditRes, err error) {
err = service.SysImWorkbench().Edit(ctx, &req.ImWorkbenchAppEditInp)
return
}
func (c *cImWorkbench) Delete(ctx context.Context, req *imworkbench.DeleteReq) (res *imworkbench.DeleteRes, err error) {
err = service.SysImWorkbench().Delete(ctx, &req.ImWorkbenchAppDeleteInp)
return
}
func (c *cImWorkbench) Reorder(ctx context.Context, req *imworkbench.ReorderReq) (res *imworkbench.ReorderRes, err error) {
err = service.SysImWorkbench().Reorder(ctx, &req.ImWorkbenchAppReorderInp)
return
}
@@ -0,0 +1,166 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
const imAbuseReasonsTable = "im_abuse_reasons"
type sSysImAbuseReason struct{}
func NewSysImAbuseReason() *sSysImAbuseReason { return &sSysImAbuseReason{} }
func init() {
service.RegisterSysImAbuseReason(NewSysImAbuseReason())
}
func (s *sSysImAbuseReason) List(ctx context.Context, in *sysin.ImAbuseReasonListInp) (list []*sysin.ImAbuseReasonModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaults(ctx, db); err != nil {
return
}
mod := s.model(ctx, db)
if in.TargetType != "" {
mod = mod.Where("target_type", in.TargetType)
}
if in.ReasonCode != "" {
mod = mod.WhereLike("reason_code", "%"+in.ReasonCode+"%")
}
if in.Enabled != nil {
mod = mod.Where("enabled", *in.Enabled)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("target_type").OrderAsc("sort_order").OrderAsc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM举报原因失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImAbuseReason) Edit(ctx context.Context, in *sysin.ImAbuseReasonEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaults(ctx, db); err != nil {
return
}
data := g.Map{
"target_type": in.TargetType,
"reason_code": in.ReasonCode,
"label": in.Label,
"requires_description": in.RequiresDescription,
"allows_evidence": in.AllowsEvidence,
"sort_order": in.SortOrder,
"enabled": in.Enabled,
"updated_at": gtime.Now(),
}
action := "im_abuse_reason_create"
targetId := in.Id
if in.Id > 0 {
if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新IM举报原因失败,请稍后重试!")
}
action = "im_abuse_reason_update"
} else {
data["created_at"] = gtime.Now()
if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "新增IM举报原因失败,请稍后重试!")
}
idValue, err := db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Fields("id").
Where("target_type", in.TargetType).Where("reason_code", in.ReasonCode).Value()
if err != nil {
return gerror.Wrap(err, "获取IM举报原因ID失败,请稍后重试!")
}
targetId = idValue.Int64()
}
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_abuse_reason", targetId, "success", g.Map{
"target_type": in.TargetType, "reason_code": in.ReasonCode, "enabled": in.Enabled,
})
return nil
}
func (s *sSysImAbuseReason) Delete(ctx context.Context, in *sysin.ImAbuseReasonDeleteInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).WherePri(in.Id).
Data(g.Map{"enabled": 0, "updated_at": gtime.Now()}).Update(); err != nil {
return gerror.Wrap(err, "禁用IM举报原因失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_abuse_reason_disable", "im_abuse_reason", in.Id, "success", nil)
return nil
}
func (s *sSysImAbuseReason) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Fields(
"id",
"target_type",
"reason_code",
"label",
"requires_description",
"allows_evidence",
"sort_order",
"enabled",
"created_at",
"updated_at",
)
}
func (s *sSysImAbuseReason) ensureDefaults(ctx context.Context, db gdb.DB) (err error) {
defaults := []g.Map{
{"target_type": consts.ImAbuseTargetTypeUser, "reason_code": "fraud", "label": "诈骗欺诈", "requires_description": 0, "allows_evidence": 1, "sort_order": 10},
{"target_type": consts.ImAbuseTargetTypeUser, "reason_code": "harassment", "label": "骚扰辱骂", "requires_description": 0, "allows_evidence": 1, "sort_order": 20},
{"target_type": consts.ImAbuseTargetTypeUser, "reason_code": "illegal", "label": "违法违规", "requires_description": 1, "allows_evidence": 1, "sort_order": 30},
{"target_type": consts.ImAbuseTargetTypeGroup, "reason_code": "fraud", "label": "诈骗欺诈", "requires_description": 0, "allows_evidence": 1, "sort_order": 10},
{"target_type": consts.ImAbuseTargetTypeGroup, "reason_code": "spam", "label": "垃圾广告", "requires_description": 0, "allows_evidence": 1, "sort_order": 20},
{"target_type": consts.ImAbuseTargetTypeGroup, "reason_code": "illegal", "label": "违法违规", "requires_description": 1, "allows_evidence": 1, "sort_order": 30},
{"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "fraud", "label": "诈骗欺诈", "requires_description": 0, "allows_evidence": 1, "sort_order": 10},
{"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "spam", "label": "垃圾广告", "requires_description": 0, "allows_evidence": 1, "sort_order": 20},
{"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "harassment", "label": "骚扰辱骂", "requires_description": 0, "allows_evidence": 1, "sort_order": 30},
{"target_type": consts.ImAbuseTargetTypeMessage, "reason_code": "illegal", "label": "违法违规", "requires_description": 1, "allows_evidence": 1, "sort_order": 40},
}
now := gtime.Now()
for _, item := range defaults {
var count int
if count, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).
Where("target_type", item["target_type"]).Where("reason_code", item["reason_code"]).Count(); err != nil {
return gerror.Wrap(err, "检查IM举报默认原因失败,请稍后重试!")
}
if count > 0 {
continue
}
item["enabled"] = 1
item["created_at"] = now
item["updated_at"] = now
if _, err = db.Model(imAbuseReasonsTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil {
return gerror.Wrap(err, "初始化IM举报默认原因失败,请稍后重试!")
}
}
return nil
}
+150
View File
@@ -0,0 +1,150 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
const imAppReleasesTable = "im_app_releases"
type sSysImAppRelease struct{}
func NewSysImAppRelease() *sSysImAppRelease { return &sSysImAppRelease{} }
func init() {
service.RegisterSysImAppRelease(NewSysImAppRelease())
}
func (s *sSysImAppRelease) List(ctx context.Context, in *sysin.ImAppReleaseListInp) (list []*sysin.ImAppReleaseModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaults(ctx, db); err != nil {
return
}
mod := s.model(ctx, db)
if in.Platform != "" {
mod = mod.Where("platform", in.Platform)
}
if in.Channel != "" {
mod = mod.WhereLike("channel", "%"+in.Channel+"%")
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("platform").OrderAsc("channel")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM客户端版本策略失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImAppRelease) Edit(ctx context.Context, in *sysin.ImAppReleaseEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaults(ctx, db); err != nil {
return
}
data := g.Map{
"platform": in.Platform,
"channel": in.Channel,
"latest_version": in.LatestVersion,
"min_supported_version": in.MinSupportedVersion,
"update_type": in.UpdateType,
"release_notes": in.ReleaseNotes,
"store_url": in.StoreUrl,
"package_url": in.PackageUrl,
"checksum": in.Checksum,
"remind_after_seconds": in.RemindAfterSeconds,
"published_at": gtime.Now(),
"updated_at": gtime.Now(),
}
action := "im_app_release_create"
targetId := in.Id
if in.Id > 0 {
if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新IM客户端版本策略失败,请稍后重试!")
}
action = "im_app_release_update"
} else {
data["created_at"] = gtime.Now()
if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "新增IM客户端版本策略失败,请稍后重试!")
}
idValue, err := db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields("id").
Where("platform", in.Platform).Where("channel", in.Channel).Value()
if err != nil {
return gerror.Wrap(err, "获取IM客户端版本策略ID失败,请稍后重试!")
}
targetId = idValue.Int64()
}
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_app_release", targetId, "success", g.Map{
"platform": in.Platform, "channel": in.Channel, "update_type": in.UpdateType,
})
return nil
}
func (s *sSysImAppRelease) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model(imAppReleasesTable).Safe().Ctx(ctx).Fields(
"id",
"platform",
"channel",
"latest_version",
"min_supported_version",
"update_type",
"release_notes",
"store_url",
"package_url",
"checksum",
"remind_after_seconds",
"published_at",
"created_at",
"updated_at",
)
}
func (s *sSysImAppRelease) ensureDefaults(ctx context.Context, db gdb.DB) (err error) {
defaults := []g.Map{
{"platform": consts.ImAppPlatformIos, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0},
{"platform": consts.ImAppPlatformAndroid, "channel": "stable", "latest_version": "1.0.0", "min_supported_version": "1.0.0", "update_type": consts.ImAppUpdateTypeNone, "release_notes": "当前已是最新版本。", "remind_after_seconds": 0},
}
now := gtime.Now()
for _, item := range defaults {
var count int
if count, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).
Where("platform", item["platform"]).Where("channel", item["channel"]).Count(); err != nil {
return gerror.Wrap(err, "检查IM客户端默认版本策略失败,请稍后重试!")
}
if count > 0 {
continue
}
item["store_url"] = ""
item["package_url"] = ""
item["checksum"] = ""
item["published_at"] = now
item["created_at"] = now
item["updated_at"] = now
if _, err = db.Model(imAppReleasesTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil {
return gerror.Wrap(err, "初始化IM客户端默认版本策略失败,请稍后重试!")
}
}
return nil
}
@@ -0,0 +1,112 @@
package sys
import (
"context"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
const imFeedbackConfigTable = "im_feedback_config"
type sSysImFeedbackConfig struct{}
func NewSysImFeedbackConfig() *sSysImFeedbackConfig { return &sSysImFeedbackConfig{} }
func init() {
service.RegisterSysImFeedbackConfig(NewSysImFeedbackConfig())
}
func (s *sSysImFeedbackConfig) Get(ctx context.Context, in *sysin.ImFeedbackConfigGetInp) (res *sysin.ImFeedbackConfigModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
if err = s.ensureDefault(ctx, db); err != nil {
return nil, err
}
if err = s.model(ctx, db).OrderAsc("id").Limit(1).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取IM意见反馈配置失败,请稍后重试!")
}
if res != nil {
res.TenantId = tenant.Id
res.TenantName = tenant.Name
}
return
}
func (s *sSysImFeedbackConfig) Update(ctx context.Context, in *sysin.ImFeedbackConfigUpdateInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return err
}
if err = s.ensureDefault(ctx, db); err != nil {
return err
}
categories := `["bug","suggestion","complaint","other"]`
if in.Categories != nil && in.Categories.String() != "" && in.Categories.String() != "null" {
categories = in.Categories.String()
}
attachmentPolicy := `{"max_files":5,"purpose":"feedback"}`
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
attachmentPolicy = in.AttachmentPolicy.String()
}
if _, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Where("id", 1).Data(g.Map{
"categories": categories,
"max_length": in.MaxLength,
"attachment_policy": attachmentPolicy,
"allow_contact": in.AllowContact,
"diagnostics_notice": in.DiagnosticsNotice,
"updated_at": gtime.Now(),
}).Update(); err != nil {
return gerror.Wrap(err, "更新IM意见反馈配置失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_feedback_config_update", "im_feedback_config", 1, "success", g.Map{"allow_contact": in.AllowContact})
return nil
}
func (s *sSysImFeedbackConfig) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Fields(
"id",
"CAST(categories AS CHAR) categories",
"max_length",
"CAST(attachment_policy AS CHAR) attachment_policy",
"allow_contact",
"diagnostics_notice",
"updated_at",
)
}
func (s *sSysImFeedbackConfig) ensureDefault(ctx context.Context, db gdb.DB) (err error) {
var count int
if count, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Where("id", 1).Count(); err != nil {
return gerror.Wrap(err, "检查IM意见反馈默认配置失败,请稍后重试!")
}
if count > 0 {
return nil
}
_, err = db.Model(imFeedbackConfigTable).Safe().Ctx(ctx).Data(g.Map{
"id": 1,
"categories": `["bug","suggestion","complaint","other"]`,
"max_length": 1000,
"attachment_policy": `{"max_files":5,"purpose":"feedback"}`,
"allow_contact": 1,
"diagnostics_notice": "仅在你同意时上传脱敏诊断信息。",
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Insert()
if err != nil {
return gerror.Wrap(err, "初始化IM意见反馈默认配置失败,请稍后重试!")
}
return nil
}
@@ -0,0 +1,121 @@
package sys
import (
"context"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
const imMailboxConfigTable = "im_mailbox_config"
type sSysImMailboxConfig struct{}
func NewSysImMailboxConfig() *sSysImMailboxConfig { return &sSysImMailboxConfig{} }
func init() {
service.RegisterSysImMailboxConfig(NewSysImMailboxConfig())
}
func (s *sSysImMailboxConfig) Get(ctx context.Context, in *sysin.ImMailboxConfigGetInp) (res *sysin.ImMailboxConfigModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
if err = s.ensureDefault(ctx, db); err != nil {
return nil, err
}
if err = s.model(ctx, db).Where("id", 1).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取IM总经理信箱配置失败,请稍后重试!")
}
if res != nil {
res.TenantId = tenant.Id
res.TenantName = tenant.Name
}
return
}
func (s *sSysImMailboxConfig) Update(ctx context.Context, in *sysin.ImMailboxConfigUpdateInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return err
}
if err = s.ensureDefault(ctx, db); err != nil {
return err
}
requiredFields := `["subject","content"]`
if in.RequiredFields != nil && in.RequiredFields.String() != "" && in.RequiredFields.String() != "null" {
requiredFields = in.RequiredFields.String()
}
attachmentPolicy := `{"max_files":5,"purpose":"executive_mailbox"}`
if in.AttachmentPolicy != nil && in.AttachmentPolicy.String() != "" && in.AttachmentPolicy.String() != "null" {
attachmentPolicy = in.AttachmentPolicy.String()
}
if _, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Where("id", 1).Data(g.Map{
"enabled": in.Enabled,
"allow_anonymous": in.AllowAnonymous,
"required_fields": requiredFields,
"max_length": in.MaxLength,
"attachment_policy": attachmentPolicy,
"notice_title": in.NoticeTitle,
"notice_content": in.NoticeContent,
"notice_updated_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Update(); err != nil {
return gerror.Wrap(err, "更新IM总经理信箱配置失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_mailbox_config_update", "im_mailbox_config", 1, "success", g.Map{"enabled": in.Enabled})
return nil
}
func (s *sSysImMailboxConfig) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Fields(
"id",
"enabled",
"allow_anonymous",
"CAST(required_fields AS CHAR) required_fields",
"max_length",
"CAST(attachment_policy AS CHAR) attachment_policy",
"notice_title",
"notice_content",
"notice_updated_at",
"updated_at",
)
}
func (s *sSysImMailboxConfig) ensureDefault(ctx context.Context, db gdb.DB) (err error) {
var count int
if count, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Where("id", 1).Count(); err != nil {
return gerror.Wrap(err, "检查IM总经理信箱默认配置失败,请稍后重试!")
}
if count > 0 {
return nil
}
_, err = db.Model(imMailboxConfigTable).Safe().Ctx(ctx).Data(g.Map{
"id": 1,
"enabled": 1,
"allow_anonymous": 1,
"required_fields": `["subject","content"]`,
"max_length": 5000,
"attachment_policy": `{"max_files":5,"purpose":"executive_mailbox"}`,
"notice_title": "总经理信箱",
"notice_content": "请如实填写诉求,匿名提交将按企业政策保护展示身份。",
"notice_updated_at": gtime.Now(),
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Insert()
if err != nil {
return gerror.Wrap(err, "初始化IM总经理信箱默认配置失败,请稍后重试!")
}
return nil
}
+194
View File
@@ -0,0 +1,194 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
const imWorkbenchAppsTable = "im_workbench_apps"
type sSysImWorkbench struct{}
func NewSysImWorkbench() *sSysImWorkbench { return &sSysImWorkbench{} }
func init() {
service.RegisterSysImWorkbench(NewSysImWorkbench())
}
func (s *sSysImWorkbench) List(ctx context.Context, in *sysin.ImWorkbenchAppListInp) (list []*sysin.ImWorkbenchAppModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaultApps(ctx, db); err != nil {
return
}
mod := s.model(ctx, db)
if in.Code != "" {
mod = mod.WhereLike("code", "%"+in.Code+"%")
}
if in.Name != "" {
mod = mod.WhereLike("name", "%"+in.Name+"%")
}
if in.Enabled != nil {
mod = mod.Where("enabled", *in.Enabled)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("sort_order").OrderAsc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM工作台模块失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImWorkbench) Edit(ctx context.Context, in *sysin.ImWorkbenchAppEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaultApps(ctx, db); err != nil {
return
}
data := g.Map{
"code": in.Code,
"name": in.Name,
"icon_file_id": in.IconFileId,
"route": in.Route,
"sort_order": in.SortOrder,
"unread_key": in.UnreadKey,
"enabled": in.Enabled,
"permission": in.Permission,
"updated_at": gtime.Now(),
}
action := "im_workbench_app_create"
targetId := in.Id
if in.Id > 0 {
if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新IM工作台模块失败,请稍后重试!")
}
action = "im_workbench_app_update"
} else {
data["created_at"] = gtime.Now()
if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "新增IM工作台模块失败,请稍后重试!")
}
idValue, err := db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Fields("id").Where("code", in.Code).Value()
if err != nil {
return gerror.Wrap(err, "获取IM工作台模块ID失败,请稍后重试!")
}
targetId = idValue.Int64()
}
_ = writeImAdminAudit(ctx, tenant.Id, action, "im_workbench_app", targetId, "success", g.Map{
"code": in.Code, "route": in.Route, "enabled": in.Enabled, "permission": in.Permission,
})
return nil
}
func (s *sSysImWorkbench) Delete(ctx context.Context, in *sysin.ImWorkbenchAppDeleteInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaultApps(ctx, db); err != nil {
return
}
mod := db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx)
if in.Id > 0 {
mod = mod.WherePri(in.Id)
} else {
mod = mod.Where("code", in.Code)
}
if _, err = mod.Data(g.Map{"enabled": consts.ImWorkbenchAppDisabled, "updated_at": gtime.Now()}).Update(); err != nil {
return gerror.Wrap(err, "禁用IM工作台模块失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_disable", "im_workbench_app", in.Id, "success", g.Map{"code": in.Code})
return nil
}
func (s *sSysImWorkbench) Reorder(ctx context.Context, in *sysin.ImWorkbenchAppReorderInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
if err != nil {
return
}
if err = s.ensureDefaultApps(ctx, db); err != nil {
return
}
if err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
for _, item := range in.Items {
if _, e := tx.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item.Code).
Data(g.Map{"sort_order": item.SortOrder, "updated_at": gtime.Now()}).Update(); e != nil {
return e
}
}
return nil
}); err != nil {
return gerror.Wrap(err, "调整IM工作台模块排序失败,请稍后重试!")
}
_ = writeImAdminAudit(ctx, tenant.Id, "im_workbench_app_reorder", "im_workbench_app", 0, "success", g.Map{"count": len(in.Items)})
return nil
}
func (s *sSysImWorkbench) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Fields(
"id",
"code",
"name",
"icon_file_id",
"route",
"sort_order",
"unread_key",
"enabled",
"permission",
"created_at",
"updated_at",
)
}
func (s *sSysImWorkbench) ensureDefaultApps(ctx context.Context, db gdb.DB) (err error) {
defaults := []g.Map{
{"code": "organization", "name": "组织架构", "route": consts.ImWorkbenchRouteOrganization, "sort_order": 10, "unread_key": "organization", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
{"code": "meetings", "name": "会议通知", "route": consts.ImWorkbenchRouteMeetings, "sort_order": 20, "unread_key": "meetings", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
{"code": "daily_reports", "name": "日报", "route": consts.ImWorkbenchRouteDailyReports, "sort_order": 30, "unread_key": "daily_reports", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
{"code": "monthly_reports", "name": "月报", "route": consts.ImWorkbenchRouteMonthlyReports, "sort_order": 40, "unread_key": "monthly_reports", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
{"code": "executive_mailbox", "name": "总经理信箱", "route": consts.ImWorkbenchRouteExecutiveMailbox, "sort_order": 50, "unread_key": "executive_mailbox", "enabled": consts.ImWorkbenchAppEnabled, "permission": consts.ImWorkbenchPermissionEnabled},
}
now := gtime.Now()
for _, item := range defaults {
var count int
if count, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Where("code", item["code"]).Count(); err != nil {
return gerror.Wrap(err, "检查IM工作台默认模块失败,请稍后重试!")
}
if count > 0 {
continue
}
item["created_at"] = now
item["updated_at"] = now
if _, err = db.Model(imWorkbenchAppsTable).Safe().Ctx(ctx).Data(item).Insert(); err != nil {
return gerror.Wrap(err, "初始化IM工作台默认模块失败,请稍后重试!")
}
}
return nil
}
@@ -0,0 +1,96 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImAbuseReasonListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
TargetType string `json:"targetType" dc:"对象类型"`
ReasonCode string `json:"reasonCode" dc:"原因编码"`
Enabled *int `json:"enabled" dc:"启用状态"`
}
func (in *ImAbuseReasonListInp) Filter(ctx context.Context) (err error) {
in.TargetType = strings.TrimSpace(in.TargetType)
in.ReasonCode = strings.TrimSpace(in.ReasonCode)
if in.TargetType != "" && !dict.HasOptionKey(consts.ImAbuseTargetTypeOptions, in.TargetType) {
return gerror.New("举报对象类型不合法")
}
if in.Enabled != nil && !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, *in.Enabled) {
return gerror.New("启用状态不合法")
}
return nil
}
type ImAbuseReasonModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
TargetType string `json:"targetType" dc:"对象类型"`
ReasonCode string `json:"reasonCode" dc:"原因编码"`
Label string `json:"label" dc:"展示文案"`
RequiresDescription int `json:"requiresDescription" dc:"要求描述"`
AllowsEvidence int `json:"allowsEvidence" dc:"允许证据"`
SortOrder int `json:"sortOrder" dc:"排序"`
Enabled int `json:"enabled" dc:"启用状态"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImAbuseReasonEditInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" dc:"ID"`
TargetType string `json:"targetType" v:"required#对象类型不能为空" dc:"对象类型"`
ReasonCode string `json:"reasonCode" v:"required#原因编码不能为空" dc:"原因编码"`
Label string `json:"label" v:"required#展示文案不能为空" dc:"展示文案"`
RequiresDescription int `json:"requiresDescription" dc:"要求描述"`
AllowsEvidence int `json:"allowsEvidence" dc:"允许证据"`
SortOrder int `json:"sortOrder" dc:"排序"`
Enabled int `json:"enabled" dc:"启用状态"`
}
func (in *ImAbuseReasonEditInp) Filter(ctx context.Context) (err error) {
in.TargetType = strings.TrimSpace(in.TargetType)
in.ReasonCode = strings.TrimSpace(in.ReasonCode)
in.Label = strings.TrimSpace(in.Label)
if !dict.HasOptionKey(consts.ImAbuseTargetTypeOptions, in.TargetType) {
return gerror.New("举报对象类型不合法")
}
if in.ReasonCode == "" || strings.ContainsAny(in.ReasonCode, " \t\r\n/") || len([]rune(in.ReasonCode)) > 64 {
return gerror.New("原因编码不能为空,且不能包含空格或斜杠")
}
if in.Label == "" || len([]rune(in.Label)) > 128 {
return gerror.New("展示文案不能为空且不能超过128个字符")
}
if !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, in.RequiresDescription) {
return gerror.New("要求描述开关不合法")
}
if !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, in.AllowsEvidence) {
return gerror.New("允许证据开关不合法")
}
if !dict.HasOptionKey(consts.ImAbuseReasonEnabledOptions, in.Enabled) {
return gerror.New("启用状态不合法")
}
return nil
}
type ImAbuseReasonDeleteInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" v:"required|min:1#请选择要禁用的举报原因|请选择要禁用的举报原因" dc:"ID"`
}
func (in *ImAbuseReasonDeleteInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("请选择要禁用的举报原因")
}
return nil
}
@@ -0,0 +1,92 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImAppReleaseListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Platform string `json:"platform" dc:"平台"`
Channel string `json:"channel" dc:"渠道"`
}
func (in *ImAppReleaseListInp) Filter(ctx context.Context) (err error) {
in.Platform = strings.TrimSpace(in.Platform)
in.Channel = strings.TrimSpace(in.Channel)
if in.Platform != "" && !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) {
return gerror.New("客户端平台不合法")
}
return nil
}
type ImAppReleaseModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
Platform string `json:"platform" dc:"平台"`
Channel string `json:"channel" dc:"渠道"`
LatestVersion string `json:"latestVersion" dc:"最新版本"`
MinSupportedVersion string `json:"minSupportedVersion" dc:"最低支持版本"`
UpdateType string `json:"updateType" dc:"更新类型"`
ReleaseNotes string `json:"releaseNotes" dc:"更新说明"`
StoreUrl string `json:"storeUrl" dc:"商店地址"`
PackageUrl string `json:"packageUrl" dc:"安装包地址"`
Checksum string `json:"checksum" dc:"校验值"`
RemindAfterSeconds int `json:"remindAfterSeconds" dc:"再次提醒秒数"`
PublishedAt *gtime.Time `json:"publishedAt" dc:"发布时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImAppReleaseEditInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" dc:"ID"`
Platform string `json:"platform" v:"required#平台不能为空" dc:"平台"`
Channel string `json:"channel" v:"required#渠道不能为空" dc:"渠道"`
LatestVersion string `json:"latestVersion" v:"required#最新版本不能为空" dc:"最新版本"`
MinSupportedVersion string `json:"minSupportedVersion" v:"required#最低支持版本不能为空" dc:"最低支持版本"`
UpdateType string `json:"updateType" v:"required#更新类型不能为空" dc:"更新类型"`
ReleaseNotes string `json:"releaseNotes" dc:"更新说明"`
StoreUrl string `json:"storeUrl" dc:"商店地址"`
PackageUrl string `json:"packageUrl" dc:"安装包地址"`
Checksum string `json:"checksum" dc:"校验值"`
RemindAfterSeconds int `json:"remindAfterSeconds" dc:"再次提醒秒数"`
}
func (in *ImAppReleaseEditInp) Filter(ctx context.Context) (err error) {
in.Platform = strings.TrimSpace(in.Platform)
in.Channel = strings.TrimSpace(in.Channel)
in.LatestVersion = strings.TrimSpace(in.LatestVersion)
in.MinSupportedVersion = strings.TrimSpace(in.MinSupportedVersion)
in.UpdateType = strings.TrimSpace(in.UpdateType)
in.StoreUrl = strings.TrimSpace(in.StoreUrl)
in.PackageUrl = strings.TrimSpace(in.PackageUrl)
in.Checksum = strings.TrimSpace(in.Checksum)
if !dict.HasOptionKey(consts.ImAppPlatformOptions, in.Platform) {
return gerror.New("客户端平台不合法")
}
if in.Channel == "" || strings.ContainsAny(in.Channel, " \t\r\n/") || len([]rune(in.Channel)) > 32 {
return gerror.New("渠道不能为空,且不能包含空格或斜杠")
}
if in.LatestVersion == "" || len([]rune(in.LatestVersion)) > 64 {
return gerror.New("最新版本不能为空且不能超过64个字符")
}
if in.MinSupportedVersion == "" || len([]rune(in.MinSupportedVersion)) > 64 {
return gerror.New("最低支持版本不能为空且不能超过64个字符")
}
if !dict.HasOptionKey(consts.ImAppUpdateTypeOptions, in.UpdateType) {
return gerror.New("更新类型不合法")
}
if in.RemindAfterSeconds < 0 {
return gerror.New("再次提醒时间不能小于0")
}
return nil
}
@@ -0,0 +1,48 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImFeedbackConfigGetInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
}
func (in *ImFeedbackConfigGetInp) Filter(ctx context.Context) (err error) { return }
type ImFeedbackConfigModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
Categories string `json:"categories" dc:"分类JSON"`
MaxLength int `json:"maxLength" dc:"最大字数"`
AttachmentPolicy string `json:"attachmentPolicy" dc:"附件策略JSON"`
AllowContact int `json:"allowContact" dc:"允许联系方式"`
DiagnosticsNotice string `json:"diagnosticsNotice" dc:"诊断信息提示"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImFeedbackConfigUpdateInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Categories *gjson.Json `json:"categories" dc:"分类JSON"`
MaxLength int `json:"maxLength" dc:"最大字数"`
AttachmentPolicy *gjson.Json `json:"attachmentPolicy" dc:"附件策略JSON"`
AllowContact int `json:"allowContact" dc:"允许联系方式"`
DiagnosticsNotice string `json:"diagnosticsNotice" v:"required#诊断提示不能为空" dc:"诊断信息提示"`
}
func (in *ImFeedbackConfigUpdateInp) Filter(ctx context.Context) (err error) {
if in.MaxLength <= 0 {
return gerror.New("最大字数必须大于0")
}
if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.AllowContact) {
return gerror.New("联系方式开关不合法")
}
return nil
}
@@ -0,0 +1,59 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImMailboxConfigGetInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
}
func (in *ImMailboxConfigGetInp) Filter(ctx context.Context) (err error) { return }
type ImMailboxConfigModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
Enabled int `json:"enabled" dc:"启用状态"`
AllowAnonymous int `json:"allowAnonymous" dc:"允许匿名"`
RequiredFields string `json:"requiredFields" dc:"必填字段JSON"`
MaxLength int `json:"maxLength" dc:"最大字数"`
AttachmentPolicy string `json:"attachmentPolicy" dc:"附件策略JSON"`
NoticeTitle string `json:"noticeTitle" dc:"告知标题"`
NoticeContent string `json:"noticeContent" dc:"告知内容"`
NoticeUpdatedAt *gtime.Time `json:"noticeUpdatedAt" dc:"告知更新时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImMailboxConfigUpdateInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Enabled int `json:"enabled" dc:"启用状态"`
AllowAnonymous int `json:"allowAnonymous" dc:"允许匿名"`
RequiredFields *gjson.Json `json:"requiredFields" dc:"必填字段JSON"`
MaxLength int `json:"maxLength" dc:"最大字数"`
AttachmentPolicy *gjson.Json `json:"attachmentPolicy" dc:"附件策略JSON"`
NoticeTitle string `json:"noticeTitle" v:"required#告知标题不能为空" dc:"告知标题"`
NoticeContent string `json:"noticeContent" v:"required#告知内容不能为空" dc:"告知内容"`
}
func (in *ImMailboxConfigUpdateInp) Filter(ctx context.Context) (err error) {
if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.Enabled) {
return gerror.New("启用状态不合法")
}
if !dict.HasOptionKey(consts.ImFilePolicySwitchOptions, in.AllowAnonymous) {
return gerror.New("匿名开关不合法")
}
if in.MaxLength <= 0 {
return gerror.New("最大字数必须大于0")
}
if len([]rune(in.NoticeTitle)) > 128 {
return gerror.New("告知标题不能超过128个字符")
}
return nil
}
@@ -0,0 +1,141 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImWorkbenchAppListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Code string `json:"code" dc:"模块编码"`
Name string `json:"name" dc:"模块名称"`
Enabled *int `json:"enabled" dc:"启用状态"`
}
func (in *ImWorkbenchAppListInp) Filter(ctx context.Context) (err error) {
in.Code = strings.TrimSpace(in.Code)
in.Name = strings.TrimSpace(in.Name)
if in.Enabled != nil && !dict.HasOptionKey(consts.ImWorkbenchAppEnabledOptions, *in.Enabled) {
return gerror.New("启用状态不合法")
}
return nil
}
type ImWorkbenchAppModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" dc:"模块编码"`
Name string `json:"name" dc:"模块名称"`
IconFileId string `json:"iconFileId" dc:"图标文件ID"`
Route string `json:"route" dc:"客户端路由"`
SortOrder int `json:"sortOrder" dc:"排序"`
UnreadKey string `json:"unreadKey" dc:"未读角标Key"`
Enabled int `json:"enabled" dc:"启用状态"`
Permission string `json:"permission" dc:"权限策略"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImWorkbenchAppEditInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" v:"required#模块编码不能为空" dc:"模块编码"`
Name string `json:"name" v:"required#模块名称不能为空" dc:"模块名称"`
IconFileId string `json:"iconFileId" dc:"图标文件ID"`
Route string `json:"route" v:"required#客户端路由不能为空" dc:"客户端路由"`
SortOrder int `json:"sortOrder" dc:"排序"`
UnreadKey string `json:"unreadKey" dc:"未读角标Key"`
Enabled int `json:"enabled" dc:"启用状态"`
Permission string `json:"permission" dc:"权限策略"`
}
func (in *ImWorkbenchAppEditInp) Filter(ctx context.Context) (err error) {
in.Code = strings.TrimSpace(in.Code)
in.Name = strings.TrimSpace(in.Name)
in.IconFileId = strings.TrimSpace(in.IconFileId)
in.Route = strings.TrimSpace(in.Route)
in.UnreadKey = strings.TrimSpace(in.UnreadKey)
in.Permission = strings.TrimSpace(in.Permission)
if in.Code == "" {
return gerror.New("模块编码不能为空")
}
if strings.ContainsAny(in.Code, " \t\r\n/") {
return gerror.New("模块编码不能包含空格或斜杠")
}
if len([]rune(in.Code)) > 64 {
return gerror.New("模块编码不能超过64个字符")
}
if in.Name == "" {
return gerror.New("模块名称不能为空")
}
if len([]rune(in.Name)) > 128 {
return gerror.New("模块名称不能超过128个字符")
}
if !dict.HasOptionKey(consts.ImWorkbenchRouteOptions, in.Route) {
return gerror.New("客户端路由不在当前IM服务端白名单内")
}
if !dict.HasOptionKey(consts.ImWorkbenchAppEnabledOptions, in.Enabled) {
return gerror.New("启用状态不合法")
}
if in.Permission == "" {
in.Permission = consts.ImWorkbenchPermissionEnabled
}
if !dict.HasOptionKey(consts.ImWorkbenchPermissionOptions, in.Permission) {
return gerror.New("权限策略不合法")
}
if len([]rune(in.IconFileId)) > 64 {
return gerror.New("图标文件ID不能超过64个字符")
}
if len([]rune(in.UnreadKey)) > 64 {
return gerror.New("未读角标Key不能超过64个字符")
}
return nil
}
type ImWorkbenchAppDeleteInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" dc:"模块编码"`
}
func (in *ImWorkbenchAppDeleteInp) Filter(ctx context.Context) (err error) {
in.Code = strings.TrimSpace(in.Code)
if in.Id <= 0 && in.Code == "" {
return gerror.New("请选择要禁用的工作台模块")
}
return nil
}
type ImWorkbenchAppReorderItem struct {
Code string `json:"code" dc:"模块编码"`
SortOrder int `json:"sortOrder" dc:"排序"`
}
type ImWorkbenchAppReorderInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Items []*ImWorkbenchAppReorderItem `json:"items" dc:"排序列表"`
}
func (in *ImWorkbenchAppReorderInp) Filter(ctx context.Context) (err error) {
if len(in.Items) == 0 {
return gerror.New("排序列表不能为空")
}
for _, item := range in.Items {
if item == nil {
return gerror.New("排序项不能为空")
}
item.Code = strings.TrimSpace(item.Code)
if item.Code == "" {
return gerror.New("模块编码不能为空")
}
}
return nil
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImAbuseReason) // IM举报原因
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImAppRelease) // IM版本更新
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImFeedbackConfig) // IM意见反馈配置
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImMailboxConfig) // IM总经理信箱配置
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImWorkbench) // IM工作台
}
+96
View File
@@ -347,6 +347,42 @@ type (
// TemplateUpdate 更新IM通知模板 // TemplateUpdate 更新IM通知模板
TemplateUpdate(ctx context.Context, in *sysin.ImNotificationTemplateUpdateInp) (err error) TemplateUpdate(ctx context.Context, in *sysin.ImNotificationTemplateUpdateInp) (err error)
} }
ISysImAppRelease interface {
// List 获取IM客户端版本策略列表
List(ctx context.Context, in *sysin.ImAppReleaseListInp) (list []*sysin.ImAppReleaseModel, totalCount int, err error)
// Edit 新增/编辑IM客户端版本策略
Edit(ctx context.Context, in *sysin.ImAppReleaseEditInp) (err error)
}
ISysImAbuseReason interface {
// List 获取IM举报原因列表
List(ctx context.Context, in *sysin.ImAbuseReasonListInp) (list []*sysin.ImAbuseReasonModel, totalCount int, err error)
// Edit 新增/编辑IM举报原因
Edit(ctx context.Context, in *sysin.ImAbuseReasonEditInp) (err error)
// Delete 禁用IM举报原因
Delete(ctx context.Context, in *sysin.ImAbuseReasonDeleteInp) (err error)
}
ISysImMailboxConfig interface {
// Get 获取IM总经理信箱配置
Get(ctx context.Context, in *sysin.ImMailboxConfigGetInp) (res *sysin.ImMailboxConfigModel, err error)
// Update 更新IM总经理信箱配置
Update(ctx context.Context, in *sysin.ImMailboxConfigUpdateInp) (err error)
}
ISysImFeedbackConfig interface {
// Get 获取IM意见反馈配置
Get(ctx context.Context, in *sysin.ImFeedbackConfigGetInp) (res *sysin.ImFeedbackConfigModel, err error)
// Update 更新IM意见反馈配置
Update(ctx context.Context, in *sysin.ImFeedbackConfigUpdateInp) (err error)
}
ISysImWorkbench interface {
// List 获取IM工作台模块列表
List(ctx context.Context, in *sysin.ImWorkbenchAppListInp) (list []*sysin.ImWorkbenchAppModel, totalCount int, err error)
// Edit 新增/编辑IM工作台模块
Edit(ctx context.Context, in *sysin.ImWorkbenchAppEditInp) (err error)
// Delete 禁用IM工作台模块
Delete(ctx context.Context, in *sysin.ImWorkbenchAppDeleteInp) (err error)
// Reorder 调整IM工作台模块排序
Reorder(ctx context.Context, in *sysin.ImWorkbenchAppReorderInp) (err error)
}
ISysImTaskDashboard interface { ISysImTaskDashboard interface {
// List 获取IM任务看板列表 // List 获取IM任务看板列表
List(ctx context.Context, in *sysin.ImTaskDashboardListInp) (list []*sysin.ImTaskDashboardListModel, totalCount int, err error) List(ctx context.Context, in *sysin.ImTaskDashboardListInp) (list []*sysin.ImTaskDashboardListModel, totalCount int, err error)
@@ -615,6 +651,11 @@ var (
localSysImFilePolicy ISysImFilePolicy localSysImFilePolicy ISysImFilePolicy
localSysImExternalConfig ISysImExternalConfig localSysImExternalConfig ISysImExternalConfig
localSysImNotification ISysImNotification localSysImNotification ISysImNotification
localSysImAppRelease ISysImAppRelease
localSysImAbuseReason ISysImAbuseReason
localSysImMailboxConfig ISysImMailboxConfig
localSysImFeedbackConfig ISysImFeedbackConfig
localSysImWorkbench ISysImWorkbench
localSysImTaskDashboard ISysImTaskDashboard localSysImTaskDashboard ISysImTaskDashboard
localSysImAuditLog ISysImAuditLog localSysImAuditLog ISysImAuditLog
localSysDictData ISysDictData localSysDictData ISysDictData
@@ -863,6 +904,61 @@ func RegisterSysImNotification(i ISysImNotification) {
localSysImNotification = i localSysImNotification = i
} }
func SysImAppRelease() ISysImAppRelease {
if localSysImAppRelease == nil {
panic("implement not found for interface ISysImAppRelease, forgot register?")
}
return localSysImAppRelease
}
func RegisterSysImAppRelease(i ISysImAppRelease) {
localSysImAppRelease = i
}
func SysImAbuseReason() ISysImAbuseReason {
if localSysImAbuseReason == nil {
panic("implement not found for interface ISysImAbuseReason, forgot register?")
}
return localSysImAbuseReason
}
func RegisterSysImAbuseReason(i ISysImAbuseReason) {
localSysImAbuseReason = i
}
func SysImMailboxConfig() ISysImMailboxConfig {
if localSysImMailboxConfig == nil {
panic("implement not found for interface ISysImMailboxConfig, forgot register?")
}
return localSysImMailboxConfig
}
func RegisterSysImMailboxConfig(i ISysImMailboxConfig) {
localSysImMailboxConfig = i
}
func SysImFeedbackConfig() ISysImFeedbackConfig {
if localSysImFeedbackConfig == nil {
panic("implement not found for interface ISysImFeedbackConfig, forgot register?")
}
return localSysImFeedbackConfig
}
func RegisterSysImFeedbackConfig(i ISysImFeedbackConfig) {
localSysImFeedbackConfig = i
}
func SysImWorkbench() ISysImWorkbench {
if localSysImWorkbench == nil {
panic("implement not found for interface ISysImWorkbench, forgot register?")
}
return localSysImWorkbench
}
func RegisterSysImWorkbench(i ISysImWorkbench) {
localSysImWorkbench = i
}
func SysImTaskDashboard() ISysImTaskDashboard { func SysImTaskDashboard() ISysImTaskDashboard {
if localSysImTaskDashboard == nil { if localSysImTaskDashboard == nil {
panic("implement not found for interface ISysImTaskDashboard, forgot register?") panic("implement not found for interface ISysImTaskDashboard, forgot register?")
@@ -0,0 +1,16 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '举报原因', 'imAbuseReasonIndex', 'abuse-reason/index', '', '2', '', '/imAbuseReason/list', '', '/imAbuseReason/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '82', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex');
SET @imAbuseReasonId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imAbuseReasonId, '原因编辑', 'imAbuseReasonEdit', '', '', '3', '', '/imAbuseReason/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAbuseReasonId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonEdit');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imAbuseReasonId, '原因禁用', 'imAbuseReasonDelete', '', '', '3', '', '/imAbuseReason/delete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAbuseReasonId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonDelete');
@@ -0,0 +1,12 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '版本更新', 'imAppReleaseIndex', 'app-release/index', '', '2', '', '/imAppRelease/list', '', '/imAppRelease/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '81', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIndex');
SET @imAppReleaseId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIndex' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imAppReleaseId, '版本编辑', 'imAppReleaseEdit', '', '', '3', '', '/imAppRelease/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAppReleaseId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseEdit');
@@ -0,0 +1,12 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '意见反馈配置', 'imFeedbackConfigIndex', 'feedback-config/index', '', '2', '', '/imFeedbackConfig/get', '', '/imFeedbackConfig/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '84', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex');
SET @imFeedbackConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imFeedbackConfigId, '反馈配置更新', 'imFeedbackConfigUpdate', '', '', '3', '', '/imFeedbackConfig/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imFeedbackConfigId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigUpdate');
@@ -0,0 +1,12 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '总经理信箱配置', 'imMailboxConfigIndex', 'mailbox-config/index', '', '2', '', '/imMailboxConfig/get', '', '/imMailboxConfig/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '83', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex');
SET @imMailboxConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imMailboxConfigId, '信箱配置更新', 'imMailboxConfigUpdate', '', '', '3', '', '/imMailboxConfig/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imMailboxConfigId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigUpdate');
@@ -0,0 +1,20 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '工作台配置', 'imWorkbenchIndex', 'workbench/index', '', '2', '', '/imWorkbench/list', '', '/imWorkbench/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '80', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex');
SET @imWorkbenchId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex' LIMIT 1);
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imWorkbenchId, '工作台编辑', 'imWorkbenchEdit', '', '', '3', '', '/imWorkbench/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchEdit');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imWorkbenchId, '工作台禁用', 'imWorkbenchDelete', '', '', '3', '', '/imWorkbench/delete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchDelete');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
SELECT NULL, @imWorkbenchId, '工作台排序', 'imWorkbenchReorder', '', '', '3', '', '/imWorkbench/reorder', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchReorder');
+13
View File
@@ -0,0 +1,13 @@
import { http } from '@/utils/http/axios';
export function List(params) {
return http.request({ url: '/imAbuseReason/list', method: 'GET', params });
}
export function Edit(params) {
return http.request({ url: '/imAbuseReason/edit', method: 'POST', params });
}
export function Delete(params) {
return http.request({ url: '/imAbuseReason/delete', method: 'POST', params });
}
+9
View File
@@ -0,0 +1,9 @@
import { http } from '@/utils/http/axios';
export function List(params) {
return http.request({ url: '/imAppRelease/list', method: 'GET', params });
}
export function Edit(params) {
return http.request({ url: '/imAppRelease/edit', method: 'POST', params });
}
+9
View File
@@ -0,0 +1,9 @@
import { http } from '@/utils/http/axios';
export function Get(params) {
return http.request({ url: '/imFeedbackConfig/get', method: 'GET', params });
}
export function Update(params) {
return http.request({ url: '/imFeedbackConfig/update', method: 'POST', params });
}
+9
View File
@@ -0,0 +1,9 @@
import { http } from '@/utils/http/axios';
export function Get(params) {
return http.request({ url: '/imMailboxConfig/get', method: 'GET', params });
}
export function Update(params) {
return http.request({ url: '/imMailboxConfig/update', method: 'POST', params });
}
+17
View File
@@ -0,0 +1,17 @@
import { http } from '@/utils/http/axios';
export function List(params) {
return http.request({ url: '/imWorkbench/list', method: 'GET', params });
}
export function Edit(params) {
return http.request({ url: '/imWorkbench/edit', method: 'POST', params });
}
export function Delete(params) {
return http.request({ url: '/imWorkbench/delete', method: 'POST', params });
}
export function Reorder(params) {
return http.request({ url: '/imWorkbench/reorder', method: 'POST', params });
}
+261
View File
@@ -0,0 +1,261 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="IM举报原因" />
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="handleReset"
@keyup.enter="reloadTable"
/>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.tenantId + '-' + row.targetType + '-' + row.reasonCode"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="primary"
:disabled="!hasPermission(['/imAbuseReason/edit']) || !currentTenantReady"
@click="handleCreate"
>
新增原因
</n-button>
</template>
</BasicTable>
</n-card>
<n-modal
v-model:show="showModal"
preset="dialog"
:show-icon="false"
:title="formValue.id > 0 ? '编辑举报原因' : '新增举报原因'"
:style="{ width: dialogWidth }"
>
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="left"
label-width="92"
>
<n-form-item label="对象" path="targetType">
<n-select
v-model:value="formValue.targetType"
:disabled="formValue.id > 0"
:options="targetTypeOptions"
placeholder="请选择对象类型"
/>
</n-form-item>
<n-form-item label="编码" path="reasonCode">
<n-input
v-model:value="formValue.reasonCode"
:disabled="formValue.id > 0"
placeholder="请输入原因编码"
/>
</n-form-item>
<n-form-item label="文案" path="label">
<n-input v-model:value="formValue.label" placeholder="请输入展示文案" />
</n-form-item>
<n-form-item label="排序">
<n-input-number v-model:value="formValue.sortOrder" :min="0" />
</n-form-item>
<n-form-item label="要求描述">
<n-switch v-model:value="requiresDescriptionValue" />
</n-form-item>
<n-form-item label="允许证据">
<n-switch v-model:value="allowsEvidenceValue" />
</n-form-item>
<n-form-item label="状态">
<n-switch v-model:value="enabledValue" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { useDictStore } from '@/store/modules/dict';
import { BasicForm, useForm } from '@/components/Form/index';
import { BasicTable, TableAction } from '@/components/Table';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { Delete, Edit, List } from '@/api/imAbuseReason';
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
import {
columns,
loadOptions,
loadTenantOptions,
newState,
rules,
schemas,
State,
tenantOptions,
} from './model';
const dict = useDictStore();
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const actionRef = ref();
const searchFormRef = ref<any>({});
const formRef = ref();
const showModal = ref(false);
const submitLoading = ref(false);
const formValue = ref<State>(newState(null));
const dialogWidth = computed(() => adaModalWidth(560));
const targetTypeOptions = computed(() => dict.getOptions('ImAbuseTargetTypeOptions'));
const currentTenantReady = computed(
() => !isPlatformAdmin.value || !!searchFormRef.value?.formModel?.tenantId
);
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
if (tenantSchema) {
tenantSchema.ifShow = () => isPlatformAdmin.value;
}
const requiresDescriptionValue = computed({
get: () => formValue.value.requiresDescription === 1,
set: (value: boolean) => {
formValue.value.requiresDescription = value ? 1 : 0;
},
});
const allowsEvidenceValue = computed({
get: () => formValue.value.allowsEvidence === 1,
set: (value: boolean) => {
formValue.value.allowsEvidence = value ? 1 : 0;
},
});
const enabledValue = computed({
get: () => formValue.value.enabled === 1,
set: (value: boolean) => {
formValue.value.enabled = value ? 1 : 0;
},
});
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
render(record: State) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
auth: ['/imAbuseReason/edit'],
onClick: () => handleEdit(record),
},
{
label: '禁用',
auth: ['/imAbuseReason/delete'],
disabled: record.enabled === 0,
onClick: () => handleDelete(record),
},
],
});
},
});
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
const [register, { setFieldsValue }] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 90,
schemas,
});
const loadDataTable = async (res) => {
const params = { ...searchFormRef.value?.formModel, ...res };
if (isPlatformAdmin.value && !params.tenantId)
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
return await List(params);
};
function reloadTable() {
actionRef.value?.reload();
}
async function handleReset() {
const defaults: Recordable = {};
if (isPlatformAdmin.value && tenantOptions.value.length > 0)
defaults.tenantId = tenantOptions.value[0].value;
await setFieldsValue(defaults);
reloadTable();
}
function handleCreate() {
formValue.value = newState({
tenantId: searchFormRef.value?.formModel?.tenantId || 0,
targetType: 'message',
requiresDescription: 0,
allowsEvidence: 1,
enabled: 1,
});
showModal.value = true;
}
function handleEdit(record: State) {
formValue.value = newState(record);
showModal.value = true;
}
function handleDelete(record: State) {
dialog.warning({
title: '确认禁用',
content: `确定禁用举报原因「${record.label}」吗?`,
positiveText: '禁用',
negativeText: '取消',
onPositiveClick: async () => {
await Delete({ tenantId: record.tenantId, id: record.id });
message.success('操作成功');
reloadTable();
},
});
}
async function handleSubmit() {
submitLoading.value = true;
try {
await formRef.value?.validate();
await Edit({ ...formValue.value });
message.success('操作成功');
showModal.value = false;
reloadTable();
} catch (e: any) {
message.error(e.message || '操作失败');
} finally {
submitLoading.value = false;
}
}
onMounted(async () => {
loadOptions();
if (isPlatformAdmin.value) {
await loadTenantOptions();
if (tenantOptions.value.length > 0) {
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
} else {
message.warning('暂无可用租户');
}
}
reloadTable();
});
</script>
+149
View File
@@ -0,0 +1,149 @@
import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { List as TenantList } from '@/api/imTenant';
const dict = useDictStore();
export const tenantOptions = ref<any[]>([]);
export class State {
public tenantId = 0;
public tenantName = '';
public id = 0;
public targetType = 'message';
public reasonCode = '';
public label = '';
public requiresDescription = 0;
public allowsEvidence = 1;
public sortOrder = 0;
public enabled = 1;
public updatedAt = '';
constructor(state?: Partial<State>) {
if (state) Object.assign(this, state);
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) return cloneDeep(state);
return new State(state);
}
return new State();
}
export const schemas = ref<FormSchema[]>([
{
field: 'tenantId',
component: 'NSelect',
label: '租户',
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
placeholder: '请选择租户',
},
},
{
field: 'targetType',
component: 'NSelect',
label: '对象',
componentProps: {
clearable: true,
options: computed(() => dict.getOptions('ImAbuseTargetTypeOptions')),
placeholder: '请选择对象类型',
},
},
{
field: 'reasonCode',
component: 'NInput',
label: '编码',
componentProps: {
clearable: true,
placeholder: '请输入原因编码',
},
},
{
field: 'enabled',
component: 'NSelect',
label: '状态',
componentProps: {
clearable: true,
options: computed(() => dict.getOptions('ImAbuseReasonEnabledOptions')),
placeholder: '请选择状态',
},
},
]);
export const columns = [
{
title: '对象',
key: 'targetType',
align: 'left',
width: 100,
render(row: State) {
return dict.getLabel('ImAbuseTargetTypeOptions', row.targetType) || row.targetType;
},
},
{ title: '编码', key: 'reasonCode', align: 'left', width: 140 },
{ title: '文案', key: 'label', align: 'left', width: 160 },
{
title: '要求描述',
key: 'requiresDescription',
align: 'left',
width: 100,
render(row: State) {
return dict.getLabel('ImAbuseReasonEnabledOptions', row.requiresDescription);
},
},
{
title: '允许证据',
key: 'allowsEvidence',
align: 'left',
width: 100,
render(row: State) {
return dict.getLabel('ImAbuseReasonEnabledOptions', row.allowsEvidence);
},
},
{ title: '排序', key: 'sortOrder', align: 'left', width: 90 },
{
title: '状态',
key: 'enabled',
align: 'left',
width: 90,
render(row: State) {
return dict.getLabel('ImAbuseReasonEnabledOptions', row.enabled);
},
},
{ title: '更新时间', key: 'updatedAt', align: 'left', width: 180 },
];
export const rules = {
targetType: {
required: true,
trigger: ['blur', 'change'],
type: 'string',
message: '请选择对象类型',
},
reasonCode: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入原因编码',
},
label: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入展示文案' },
};
export function loadOptions() {
dict.loadOptions(['ImAbuseTargetTypeOptions', 'ImAbuseReasonEnabledOptions']);
}
export async function loadTenantOptions() {
const res = await TenantList({ page: 1, perPage: 100 });
tenantOptions.value = (res?.list || []).map((item) => ({
label: `${item.name}(${item.code})`,
value: item.id,
}));
return tenantOptions.value;
}
+242
View File
@@ -0,0 +1,242 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="IM版本更新" />
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="handleReset"
@keyup.enter="reloadTable"
/>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.tenantId + '-' + row.platform + '-' + row.channel"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="primary"
:disabled="!hasPermission(['/imAppRelease/edit']) || !currentTenantReady"
@click="handleCreate"
>
新增策略
</n-button>
</template>
</BasicTable>
</n-card>
<n-modal
v-model:show="showModal"
preset="dialog"
:show-icon="false"
:title="formValue.id > 0 ? '编辑版本策略' : '新增版本策略'"
:style="{ width: dialogWidth }"
>
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="left"
label-width="112"
>
<n-form-item label="平台" path="platform">
<n-select
v-model:value="formValue.platform"
:disabled="formValue.id > 0"
:options="platformOptions"
placeholder="请选择平台"
/>
</n-form-item>
<n-form-item label="渠道" path="channel">
<n-input
v-model:value="formValue.channel"
:disabled="formValue.id > 0"
placeholder="请输入渠道"
/>
</n-form-item>
<n-form-item label="最新版本" path="latestVersion">
<n-input v-model:value="formValue.latestVersion" placeholder="请输入最新版本" />
</n-form-item>
<n-form-item label="最低支持版本" path="minSupportedVersion">
<n-input v-model:value="formValue.minSupportedVersion" placeholder="请输入最低支持版本" />
</n-form-item>
<n-form-item label="更新类型" path="updateType">
<n-select
v-model:value="formValue.updateType"
:options="updateTypeOptions"
placeholder="请选择更新类型"
/>
</n-form-item>
<n-form-item label="再次提醒秒数">
<n-input-number v-model:value="formValue.remindAfterSeconds" :min="0" />
</n-form-item>
<n-form-item label="商店地址">
<n-input v-model:value="formValue.storeUrl" placeholder="请输入商店地址" />
</n-form-item>
<n-form-item label="安装包地址">
<n-input v-model:value="formValue.packageUrl" placeholder="请输入安装包地址" />
</n-form-item>
<n-form-item label="校验值">
<n-input v-model:value="formValue.checksum" placeholder="请输入安装包校验值" />
</n-form-item>
<n-form-item label="更新说明">
<n-input
v-model:value="formValue.releaseNotes"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
placeholder="请输入更新说明"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useDictStore } from '@/store/modules/dict';
import { BasicForm, useForm } from '@/components/Form/index';
import { BasicTable, TableAction } from '@/components/Table';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { Edit, List } from '@/api/imAppRelease';
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
import {
columns,
loadOptions,
loadTenantOptions,
newState,
rules,
schemas,
State,
tenantOptions,
} from './model';
const dict = useDictStore();
const message = useMessage();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const actionRef = ref();
const searchFormRef = ref<any>({});
const formRef = ref();
const showModal = ref(false);
const submitLoading = ref(false);
const formValue = ref<State>(newState(null));
const dialogWidth = computed(() => adaModalWidth(720));
const platformOptions = computed(() => dict.getOptions('ImAppPlatformOptions'));
const updateTypeOptions = computed(() => dict.getOptions('ImAppUpdateTypeOptions'));
const currentTenantReady = computed(
() => !isPlatformAdmin.value || !!searchFormRef.value?.formModel?.tenantId
);
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
if (tenantSchema) {
tenantSchema.ifShow = () => isPlatformAdmin.value;
}
const actionColumn = reactive({
width: 100,
title: '操作',
key: 'action',
fixed: 'right',
render(record: State) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
auth: ['/imAppRelease/edit'],
onClick: () => handleEdit(record),
},
],
});
},
});
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
const [register, { setFieldsValue }] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 90,
schemas,
});
const loadDataTable = async (res) => {
const params = { ...searchFormRef.value?.formModel, ...res };
if (isPlatformAdmin.value && !params.tenantId)
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
return await List(params);
};
function reloadTable() {
actionRef.value?.reload();
}
async function handleReset() {
const defaults: Recordable = {};
if (isPlatformAdmin.value && tenantOptions.value.length > 0)
defaults.tenantId = tenantOptions.value[0].value;
await setFieldsValue(defaults);
reloadTable();
}
function handleCreate() {
formValue.value = newState({
tenantId: searchFormRef.value?.formModel?.tenantId || 0,
platform: 'android',
channel: 'stable',
latestVersion: '1.0.0',
minSupportedVersion: '1.0.0',
updateType: 'none',
remindAfterSeconds: 0,
});
showModal.value = true;
}
function handleEdit(record: State) {
formValue.value = newState(record);
showModal.value = true;
}
async function handleSubmit() {
submitLoading.value = true;
try {
await formRef.value?.validate();
await Edit({ ...formValue.value });
message.success('操作成功');
showModal.value = false;
reloadTable();
} catch (e: any) {
message.error(e.message || '操作失败');
} finally {
submitLoading.value = false;
}
}
onMounted(async () => {
loadOptions();
if (isPlatformAdmin.value) {
await loadTenantOptions();
if (tenantOptions.value.length > 0) {
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
} else {
message.warning('暂无可用租户');
}
}
reloadTable();
});
</script>
+136
View File
@@ -0,0 +1,136 @@
import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { List as TenantList } from '@/api/imTenant';
const dict = useDictStore();
export const tenantOptions = ref<any[]>([]);
export class State {
public tenantId = 0;
public tenantName = '';
public id = 0;
public platform = 'android';
public channel = 'stable';
public latestVersion = '1.0.0';
public minSupportedVersion = '1.0.0';
public updateType = 'none';
public releaseNotes = '';
public storeUrl = '';
public packageUrl = '';
public checksum = '';
public remindAfterSeconds = 0;
public publishedAt = '';
public updatedAt = '';
constructor(state?: Partial<State>) {
if (state) Object.assign(this, state);
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) return cloneDeep(state);
return new State(state);
}
return new State();
}
export const schemas = ref<FormSchema[]>([
{
field: 'tenantId',
component: 'NSelect',
label: '租户',
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
placeholder: '请选择租户',
},
},
{
field: 'platform',
component: 'NSelect',
label: '平台',
componentProps: {
clearable: true,
options: computed(() => dict.getOptions('ImAppPlatformOptions')),
placeholder: '请选择平台',
},
},
{
field: 'channel',
component: 'NInput',
label: '渠道',
componentProps: {
clearable: true,
placeholder: '请输入渠道',
},
},
]);
export const columns = [
{
title: '平台',
key: 'platform',
align: 'left',
width: 100,
render(row: State) {
return dict.getLabel('ImAppPlatformOptions', row.platform) || row.platform;
},
},
{ title: '渠道', key: 'channel', align: 'left', width: 100 },
{ title: '最新版本', key: 'latestVersion', align: 'left', width: 120 },
{ title: '最低支持', key: 'minSupportedVersion', align: 'left', width: 120 },
{
title: '更新类型',
key: 'updateType',
align: 'left',
width: 110,
render(row: State) {
return dict.getLabel('ImAppUpdateTypeOptions', row.updateType) || row.updateType;
},
},
{ title: '再次提醒', key: 'remindAfterSeconds', align: 'left', width: 110 },
{ title: '商店地址', key: 'storeUrl', align: 'left', width: 220 },
{ title: '安装包地址', key: 'packageUrl', align: 'left', width: 220 },
{ title: '发布时间', key: 'publishedAt', align: 'left', width: 180 },
{ title: '更新时间', key: 'updatedAt', align: 'left', width: 180 },
];
export const rules = {
platform: { required: true, trigger: ['blur', 'change'], type: 'string', message: '请选择平台' },
channel: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入渠道' },
latestVersion: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入最新版本',
},
minSupportedVersion: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入最低支持版本',
},
updateType: {
required: true,
trigger: ['blur', 'change'],
type: 'string',
message: '请选择更新类型',
},
};
export function loadOptions() {
dict.loadOptions(['ImAppPlatformOptions', 'ImAppUpdateTypeOptions']);
}
export async function loadTenantOptions() {
const res = await TenantList({ page: 1, perPage: 100 });
tenantOptions.value = (res?.list || []).map((item) => ({
label: `${item.name}(${item.code})`,
value: item.id,
}));
return tenantOptions.value;
}
+140
View File
@@ -0,0 +1,140 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="IM意见反馈配置" />
</div>
<n-card :bordered="false" class="proCard">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="left"
label-width="120"
>
<n-form-item v-if="isPlatformAdmin" label="租户">
<n-select
v-model:value="selectedTenantId"
:options="tenantOptions"
placeholder="请选择租户"
@update:value="loadConfig"
/>
</n-form-item>
<n-form-item label="允许联系方式">
<n-switch v-model:value="allowContactValue" />
</n-form-item>
<n-form-item label="最大字数" path="maxLength">
<n-input-number v-model:value="formValue.maxLength" :min="1" />
</n-form-item>
<n-form-item label="反馈分类">
<n-input
v-model:value="formValue.categories"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入JSON数组"
/>
</n-form-item>
<n-form-item label="附件策略">
<n-input
v-model:value="formValue.attachmentPolicy"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入JSON对象"
/>
</n-form-item>
<n-form-item label="诊断提示" path="diagnosticsNotice">
<n-input
v-model:value="formValue.diagnosticsNotice"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
placeholder="请输入诊断提示"
/>
</n-form-item>
<n-form-item>
<n-space>
<n-button
type="primary"
:loading="submitLoading"
:disabled="!hasPermission(['/imFeedbackConfig/update']) || !currentTenantReady"
@click="handleSubmit"
>
保存
</n-button>
<n-button :loading="loadLoading" @click="loadConfig">刷新</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { Get, Update } from '@/api/imFeedbackConfig';
import { loadOptions, loadTenantOptions, newState, rules, State, tenantOptions } from './model';
const message = useMessage();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const formRef = ref();
const formValue = ref<State>(newState(null));
const selectedTenantId = ref(0);
const loadLoading = ref(false);
const submitLoading = ref(false);
const currentTenantReady = computed(() => !isPlatformAdmin.value || selectedTenantId.value > 0);
const allowContactValue = computed({
get: () => formValue.value.allowContact === 1,
set: (value: boolean) => {
formValue.value.allowContact = value ? 1 : 0;
},
});
async function loadConfig() {
if (!currentTenantReady.value) return;
loadLoading.value = true;
try {
const data = await Get({ tenantId: selectedTenantId.value });
formValue.value = newState(data || null);
formValue.value.tenantId = selectedTenantId.value;
} catch (e: any) {
message.error(e.message || '加载失败');
} finally {
loadLoading.value = false;
}
}
async function handleSubmit() {
submitLoading.value = true;
try {
await formRef.value?.validate();
const categories = JSON.parse(formValue.value.categories || '[]');
const attachmentPolicy = JSON.parse(formValue.value.attachmentPolicy || '{}');
await Update({
...formValue.value,
tenantId: selectedTenantId.value,
categories,
attachmentPolicy,
});
message.success('操作成功');
await loadConfig();
} catch (e: any) {
message.error(e.message || '操作失败');
} finally {
submitLoading.value = false;
}
}
onMounted(async () => {
loadOptions();
if (isPlatformAdmin.value) {
await loadTenantOptions();
if (tenantOptions.value.length > 0) selectedTenantId.value = tenantOptions.value[0].value;
else message.warning('暂无可用租户');
}
await loadConfig();
});
</script>
+58
View File
@@ -0,0 +1,58 @@
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { List as TenantList } from '@/api/imTenant';
import { useDictStore } from '@/store/modules/dict';
export const tenantOptions = ref<any[]>([]);
export class State {
public tenantId = 0;
public tenantName = '';
public id = 1;
public categories = '["bug","suggestion","complaint","other"]';
public maxLength = 1000;
public attachmentPolicy = '{"max_files":5,"purpose":"feedback"}';
public allowContact = 1;
public diagnosticsNotice = '仅在你同意时上传脱敏诊断信息。';
public updatedAt = '';
constructor(state?: Partial<State>) {
if (state) Object.assign(this, state);
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) return cloneDeep(state);
return new State(state);
}
return new State();
}
export const rules = {
diagnosticsNotice: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入诊断提示',
},
maxLength: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入最大字数',
},
};
export function loadOptions() {
useDictStore().loadOptions(['ImFilePolicySwitchOptions']);
}
export async function loadTenantOptions() {
const res = await TenantList({ page: 1, perPage: 100 });
tenantOptions.value = (res?.list || []).map((item) => ({
label: `${item.name}(${item.code})`,
value: item.id,
}));
return tenantOptions.value;
}
+152
View File
@@ -0,0 +1,152 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="IM总经理信箱配置" />
</div>
<n-card :bordered="false" class="proCard">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="left"
label-width="120"
>
<n-form-item v-if="isPlatformAdmin" label="租户">
<n-select
v-model:value="selectedTenantId"
:options="tenantOptions"
placeholder="请选择租户"
@update:value="loadConfig"
/>
</n-form-item>
<n-form-item label="启用信箱">
<n-switch v-model:value="enabledValue" />
</n-form-item>
<n-form-item label="允许匿名">
<n-switch v-model:value="allowAnonymousValue" />
</n-form-item>
<n-form-item label="最大字数" path="maxLength">
<n-input-number v-model:value="formValue.maxLength" :min="1" />
</n-form-item>
<n-form-item label="必填字段">
<n-input
v-model:value="formValue.requiredFields"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入JSON数组"
/>
</n-form-item>
<n-form-item label="附件策略">
<n-input
v-model:value="formValue.attachmentPolicy"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
placeholder="请输入JSON对象"
/>
</n-form-item>
<n-form-item label="告知标题" path="noticeTitle">
<n-input v-model:value="formValue.noticeTitle" placeholder="请输入告知标题" />
</n-form-item>
<n-form-item label="告知内容" path="noticeContent">
<n-input
v-model:value="formValue.noticeContent"
type="textarea"
:autosize="{ minRows: 5, maxRows: 10 }"
placeholder="请输入告知内容"
/>
</n-form-item>
<n-form-item>
<n-space>
<n-button
type="primary"
:loading="submitLoading"
:disabled="!hasPermission(['/imMailboxConfig/update']) || !currentTenantReady"
@click="handleSubmit"
>
保存
</n-button>
<n-button :loading="loadLoading" @click="loadConfig">刷新</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { Get, Update } from '@/api/imMailboxConfig';
import { loadOptions, loadTenantOptions, newState, rules, State, tenantOptions } from './model';
const message = useMessage();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const formRef = ref();
const formValue = ref<State>(newState(null));
const selectedTenantId = ref(0);
const loadLoading = ref(false);
const submitLoading = ref(false);
const currentTenantReady = computed(() => !isPlatformAdmin.value || selectedTenantId.value > 0);
const enabledValue = computed({
get: () => formValue.value.enabled === 1,
set: (value: boolean) => {
formValue.value.enabled = value ? 1 : 0;
},
});
const allowAnonymousValue = computed({
get: () => formValue.value.allowAnonymous === 1,
set: (value: boolean) => {
formValue.value.allowAnonymous = value ? 1 : 0;
},
});
async function loadConfig() {
if (!currentTenantReady.value) return;
loadLoading.value = true;
try {
const data = await Get({ tenantId: selectedTenantId.value });
formValue.value = newState(data || null);
formValue.value.tenantId = selectedTenantId.value;
} catch (e: any) {
message.error(e.message || '加载失败');
} finally {
loadLoading.value = false;
}
}
async function handleSubmit() {
submitLoading.value = true;
try {
await formRef.value?.validate();
const requiredFields = JSON.parse(formValue.value.requiredFields || '[]');
const attachmentPolicy = JSON.parse(formValue.value.attachmentPolicy || '{}');
await Update({
...formValue.value,
tenantId: selectedTenantId.value,
requiredFields,
attachmentPolicy,
});
message.success('操作成功');
await loadConfig();
} catch (e: any) {
message.error(e.message || '操作失败');
} finally {
submitLoading.value = false;
}
}
onMounted(async () => {
loadOptions();
if (isPlatformAdmin.value) {
await loadTenantOptions();
if (tenantOptions.value.length > 0) selectedTenantId.value = tenantOptions.value[0].value;
else message.warning('暂无可用租户');
}
await loadConfig();
});
</script>
+67
View File
@@ -0,0 +1,67 @@
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { List as TenantList } from '@/api/imTenant';
import { useDictStore } from '@/store/modules/dict';
export const tenantOptions = ref<any[]>([]);
export class State {
public tenantId = 0;
public tenantName = '';
public id = 1;
public enabled = 1;
public allowAnonymous = 1;
public requiredFields = '["subject","content"]';
public maxLength = 5000;
public attachmentPolicy = '{"max_files":5,"purpose":"executive_mailbox"}';
public noticeTitle = '总经理信箱';
public noticeContent = '请如实填写诉求,匿名提交将按企业政策保护展示身份。';
public noticeUpdatedAt = '';
public updatedAt = '';
constructor(state?: Partial<State>) {
if (state) Object.assign(this, state);
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) return cloneDeep(state);
return new State(state);
}
return new State();
}
export const rules = {
noticeTitle: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入告知标题',
},
noticeContent: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入告知内容',
},
maxLength: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入最大字数',
},
};
export function loadOptions() {
useDictStore().loadOptions(['ImFilePolicySwitchOptions']);
}
export async function loadTenantOptions() {
const res = await TenantList({ page: 1, perPage: 100 });
tenantOptions.value = (res?.list || []).map((item) => ({
label: `${item.name}(${item.code})`,
value: item.id,
}));
return tenantOptions.value;
}
+291
View File
@@ -0,0 +1,291 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="IM工作台" />
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="handleReset"
@keyup.enter="reloadTable"
/>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.tenantId + '-' + row.code"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-space>
<n-button
type="primary"
:disabled="!hasPermission(['/imWorkbench/edit']) || !currentTenantReady"
@click="handleCreate"
>
新增模块
</n-button>
<n-button
:disabled="!hasPermission(['/imWorkbench/reorder']) || tableRows.length === 0"
:loading="reorderLoading"
@click="handleReorder"
>
保存排序
</n-button>
</n-space>
</template>
</BasicTable>
</n-card>
<n-modal
v-model:show="showModal"
preset="dialog"
:show-icon="false"
:title="formValue.id > 0 ? '编辑工作台模块' : '新增工作台模块'"
:style="{ width: dialogWidth }"
>
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
label-placement="left"
label-width="92"
>
<n-form-item label="编码" path="code">
<n-input
v-model:value="formValue.code"
:disabled="formValue.id > 0"
placeholder="请输入模块编码"
/>
</n-form-item>
<n-form-item label="名称" path="name">
<n-input v-model:value="formValue.name" placeholder="请输入模块名称" />
</n-form-item>
<n-form-item label="路由" path="route">
<n-select
v-model:value="formValue.route"
:options="routeOptions"
placeholder="请选择客户端路由"
/>
</n-form-item>
<n-form-item label="排序">
<n-input-number v-model:value="formValue.sortOrder" :min="0" />
</n-form-item>
<n-form-item label="图标ID">
<n-input v-model:value="formValue.iconFileId" placeholder="请输入图标文件ID" />
</n-form-item>
<n-form-item label="角标Key">
<n-input v-model:value="formValue.unreadKey" placeholder="请输入未读角标Key" />
</n-form-item>
<n-form-item label="权限" path="permission">
<n-select
v-model:value="formValue.permission"
:options="permissionOptions"
placeholder="请选择权限策略"
/>
</n-form-item>
<n-form-item label="状态">
<n-switch v-model:value="enabledValue" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { useDictStore } from '@/store/modules/dict';
import { BasicForm, useForm } from '@/components/Form/index';
import { BasicTable, TableAction } from '@/components/Table';
import { usePermission } from '@/hooks/web/usePermission';
import { useUserStore } from '@/store/modules/user';
import { Delete, Edit, List, Reorder } from '@/api/imWorkbench';
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
import {
columns,
loadOptions,
loadTenantOptions,
newState,
rules,
schemas,
State,
tenantOptions,
} from './model';
const dict = useDictStore();
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const userStore = useUserStore();
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
const actionRef = ref();
const searchFormRef = ref<any>({});
const formRef = ref();
const showModal = ref(false);
const submitLoading = ref(false);
const reorderLoading = ref(false);
const tableRows = ref<State[]>([]);
const formValue = ref<State>(newState(null));
const dialogWidth = computed(() => adaModalWidth(640));
const routeOptions = computed(() => dict.getOptions('ImWorkbenchRouteOptions'));
const permissionOptions = computed(() => dict.getOptions('ImWorkbenchPermissionOptions'));
const currentTenantReady = computed(
() => !isPlatformAdmin.value || !!searchFormRef.value?.formModel?.tenantId
);
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
if (tenantSchema) {
tenantSchema.ifShow = () => isPlatformAdmin.value;
}
const enabledValue = computed({
get: () => formValue.value.enabled === 1,
set: (value: boolean) => {
formValue.value.enabled = value ? 1 : 0;
},
});
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
render(record: State) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
auth: ['/imWorkbench/edit'],
onClick: () => handleEdit(record),
},
{
label: '禁用',
auth: ['/imWorkbench/delete'],
disabled: record.enabled === 0,
onClick: () => handleDelete(record),
},
],
});
},
});
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
const [register, { setFieldsValue }] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 90,
schemas,
});
const loadDataTable = async (res) => {
const params = { ...searchFormRef.value?.formModel, ...res };
if (isPlatformAdmin.value && !params.tenantId) {
tableRows.value = [];
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
}
const data = await List(params);
tableRows.value = data?.list || [];
return data;
};
function reloadTable() {
actionRef.value?.reload();
}
async function handleReset() {
const defaults: Recordable = {};
if (isPlatformAdmin.value && tenantOptions.value.length > 0)
defaults.tenantId = tenantOptions.value[0].value;
await setFieldsValue(defaults);
reloadTable();
}
function handleCreate() {
formValue.value = newState({
tenantId: searchFormRef.value?.formModel?.tenantId || 0,
enabled: 1,
permission: 'enabled',
sortOrder: (tableRows.value.length + 1) * 10,
});
showModal.value = true;
}
function handleEdit(record: State) {
formValue.value = newState(record);
showModal.value = true;
}
function handleDelete(record: State) {
dialog.warning({
title: '确认禁用',
content: `确定禁用工作台模块「${record.name}」吗?`,
positiveText: '禁用',
negativeText: '取消',
onPositiveClick: async () => {
await Delete({ tenantId: record.tenantId, id: record.id, code: record.code });
message.success('操作成功');
reloadTable();
},
});
}
async function handleReorder() {
reorderLoading.value = true;
try {
const tenantId =
searchFormRef.value?.formModel?.tenantId || tableRows.value[0]?.tenantId || 0;
await Reorder({
tenantId,
items: tableRows.value.map((item, index) => ({
code: item.code,
sortOrder: item.sortOrder || (index + 1) * 10,
})),
});
message.success('操作成功');
reloadTable();
} catch (e: any) {
message.error(e.message || '操作失败');
} finally {
reorderLoading.value = false;
}
}
async function handleSubmit() {
submitLoading.value = true;
try {
await formRef.value?.validate();
await Edit({ ...formValue.value });
message.success('操作成功');
showModal.value = false;
reloadTable();
} catch (e: any) {
message.error(e.message || '操作失败');
} finally {
submitLoading.value = false;
}
}
onMounted(async () => {
loadOptions();
if (isPlatformAdmin.value) {
await loadTenantOptions();
if (tenantOptions.value.length > 0) {
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
} else {
message.warning('暂无可用租户');
}
}
reloadTable();
});
</script>
+143
View File
@@ -0,0 +1,143 @@
import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { List as TenantList } from '@/api/imTenant';
const dict = useDictStore();
export const tenantOptions = ref<any[]>([]);
export class State {
public tenantId = 0;
public tenantName = '';
public id = 0;
public code = '';
public name = '';
public iconFileId = '';
public route = '';
public sortOrder = 0;
public unreadKey = '';
public enabled = 1;
public permission = 'enabled';
public createdAt = '';
public updatedAt = '';
constructor(state?: Partial<State>) {
if (state) Object.assign(this, state);
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) return cloneDeep(state);
return new State(state);
}
return new State();
}
export const schemas = ref<FormSchema[]>([
{
field: 'tenantId',
component: 'NSelect',
label: '租户',
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
placeholder: '请选择租户',
},
},
{
field: 'code',
component: 'NInput',
label: '编码',
componentProps: {
clearable: true,
placeholder: '请输入模块编码',
},
},
{
field: 'name',
component: 'NInput',
label: '名称',
componentProps: {
clearable: true,
placeholder: '请输入模块名称',
},
},
{
field: 'enabled',
component: 'NSelect',
label: '状态',
componentProps: {
clearable: true,
options: computed(() => dict.getOptions('ImWorkbenchAppEnabledOptions')),
placeholder: '请选择状态',
},
},
]);
export const columns = [
{ title: '编码', key: 'code', align: 'left', width: 150 },
{ title: '名称', key: 'name', align: 'left', width: 140 },
{
title: '路由',
key: 'route',
align: 'left',
width: 160,
render(row: State) {
return dict.getLabel('ImWorkbenchRouteOptions', row.route) || row.route;
},
},
{ title: '排序', key: 'sortOrder', align: 'left', width: 90 },
{ title: '图标ID', key: 'iconFileId', align: 'left', width: 150 },
{ title: '角标Key', key: 'unreadKey', align: 'left', width: 130 },
{
title: '状态',
key: 'enabled',
align: 'left',
width: 90,
render(row: State) {
return dict.getLabel('ImWorkbenchAppEnabledOptions', row.enabled);
},
},
{
title: '权限',
key: 'permission',
align: 'left',
width: 120,
render(row: State) {
return dict.getLabel('ImWorkbenchPermissionOptions', row.permission) || row.permission;
},
},
{ title: '更新时间', key: 'updatedAt', align: 'left', width: 180 },
];
export const rules = {
code: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入模块编码' },
name: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入模块名称' },
route: { required: true, trigger: ['blur', 'change'], type: 'string', message: '请选择路由' },
permission: {
required: true,
trigger: ['blur', 'change'],
type: 'string',
message: '请选择权限策略',
},
};
export function loadOptions() {
dict.loadOptions([
'ImWorkbenchAppEnabledOptions',
'ImWorkbenchPermissionOptions',
'ImWorkbenchRouteOptions',
]);
}
export async function loadTenantOptions() {
const res = await TenantList({ page: 1, perPage: 100 });
tenantOptions.value = (res?.list || []).map((item) => ({
label: `${item.name}(${item.code})`,
value: item.id,
}));
return tenantOptions.value;
}