This commit is contained in:
likun 2025-04-28 15:56:04 +08:00
parent 7fb270308d
commit e9d01aa19f
30 changed files with 605 additions and 341 deletions

View File

@ -22,7 +22,7 @@ func initCommonResourcesRepo(db *gorm.DB) {
r(consts.ResourcesName_Account, "账号管理", repo.NewCommonResourceRepo(db, &model.Account{}), ShowMethod_Get) // 账号管理不需要在后台读写数据都是通过项目api拉 r(consts.ResourcesName_Account, "账号管理", repo.NewCommonResourceRepo(db, &model.Account{}), ShowMethod_Get) // 账号管理不需要在后台读写数据都是通过项目api拉
r(consts.ResourcesName_Role, "角色管理", repo.NewCommonResourceRepo(db, &model.Role{}), ShowMethod_Get) // 角色管理不需要在后台读写数据都是通过项目api拉 r(consts.ResourcesName_Role, "角色管理", repo.NewCommonResourceRepo(db, &model.Role{}), ShowMethod_Get) // 角色管理不需要在后台读写数据都是通过项目api拉
r(consts.ResourcesName_WhiteList, "白名单", repo.NewCommonResourceRepo(db, &model.WhiteList{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Delete) r(consts.ResourcesName_WhiteList, "白名单", repo.NewCommonResourceRepo(db, &model.WhiteList{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Delete)
r(consts.ResourcesName_Ban, "封禁管理", repo.NewCommonResourceRepo(db, &model.Ban{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Delete) r(consts.ResourcesName_Ban, "封禁管理", repo.NewCommonResourceRepo(db, &model.Ban{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete)
r(consts.ResourcesName_MailRole, "个人邮件", repo.NewCommonResourceRepo(db, &model.RoleMail{}), ShowMethod_Get|ShowMethod_Post) // 个人邮件发放就没法撤回? r(consts.ResourcesName_MailRole, "个人邮件", repo.NewCommonResourceRepo(db, &model.RoleMail{}), ShowMethod_Get|ShowMethod_Post) // 个人邮件发放就没法撤回?
r(consts.ResourcesName_MailGlobal, "全服邮件", repo.NewCommonResourceRepo(db, &model.GlobalMail{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Delete) // 直接删除,别修改了,玩家收到的更乱 r(consts.ResourcesName_MailGlobal, "全服邮件", repo.NewCommonResourceRepo(db, &model.GlobalMail{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Delete) // 直接删除,别修改了,玩家收到的更乱
r(consts.ResourcesName_Notice, "公告", repo.NewCommonResourceRepo(db, &model.Notice{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete) r(consts.ResourcesName_Notice, "公告", repo.NewCommonResourceRepo(db, &model.Notice{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete)

View File

@ -8,6 +8,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -27,6 +28,10 @@ func poToCommonDtoNo(po any) dto.CommonDtoValues {
} }
obj[ft.Name] = fo.Interface() obj[ft.Name] = fo.Interface()
if ft.Type.Name() == "Time" && ft.Type.PkgPath() == "time" {
obj[ft.Name] = fo.Interface().(time.Time).Format(time.DateTime)
}
} }
return obj return obj
@ -46,6 +51,11 @@ func getFieldTypeDtoDescInfo(projectId string, poValue reflect.Value, fieldType
Required: fieldType.Tag.Get("required") == "true", Required: fieldType.Tag.Get("required") == "true",
Choices: make([]*dto.CommonDtoFieldChoice, 0), Choices: make([]*dto.CommonDtoFieldChoice, 0),
MultiChoice: fieldType.Tag.Get("multi_choice") == "true", MultiChoice: fieldType.Tag.Get("multi_choice") == "true",
Uneditable: fieldType.Tag.Get("uneditable") == "true",
}
if f1.Key == "CreatedAt" {
f1.Name = "创建时间"
} }
if tagType := fieldType.Tag.Get("type"); tagType != "" { if tagType := fieldType.Tag.Get("type"); tagType != "" {
@ -114,7 +124,9 @@ func parseStr2FieldValue(field reflect.StructField, rawValue any) (realSetValue
case reflect.Struct: case reflect.Struct:
typeName := field.Type.Name() typeName := field.Type.Name()
if typeName == "Time" { if typeName == "Time" {
return reflect.ValueOf(time.Now()) xlog.Debugf("time content:%v", setValue)
t, _ := time.ParseInLocation("2006/01/02 15:04:05", setValue, time.Local)
return reflect.ValueOf(t)
} }
if typeName == "DeletedAt" { if typeName == "DeletedAt" {
return reflect.ValueOf(gorm.DeletedAt{}) return reflect.ValueOf(gorm.DeletedAt{})
@ -124,29 +136,43 @@ func parseStr2FieldValue(field reflect.StructField, rawValue any) (realSetValue
typeName := field.Type.String() typeName := field.Type.String()
if typeName == "[]string" { if typeName == "[]string" {
list := make([]string, 0) list := make([]string, 0)
if _, ok := rawValue.(string); ok { if rawValueStr, ok := rawValue.(string); ok {
elems := strings.Split(rawValueStr, ",")
list = append(list, elems...)
// 空的字符串 // 空的字符串
} else { } else {
for _, v := range rawValue.([]interface{}) { if rawValueList, ok := rawValue.([]string); ok {
list = append(list, v.(string)) for _, v := range rawValueList {
list = append(list, v)
}
} else {
for _, v := range rawValue.([]interface{}) {
list = append(list, v.(string))
}
} }
} }
parsedValue = list parsedValue = list
} else if typeName == "[]*model.MailAttachItem" { } else if typeName == "[]*model.MailAttachItem" {
items := make([]*model.MailAttachItem, 0) items := make([]*model.MailAttachItem, 0)
for _, itemI := range rawValue.([]interface{}) { if rawValueList, ok := rawValue.([]*model.MailAttachItem); ok {
item := &model.MailAttachItem{} for _, item := range rawValueList {
for k, vI := range itemI.(map[string]any) { items = append(items, item)
v := vI.(float64) }
if k == "id" { } else {
item.ID = int32(v) for _, itemI := range rawValue.([]interface{}) {
} else if k == "num" { item := &model.MailAttachItem{}
item.Num = int64(v) for k, vI := range itemI.(map[string]any) {
} else if k == "item_type" { v := vI.(float64)
item.ItemType = int(v) if k == "id" {
} item.ID = int32(v)
} else if k == "num" {
item.Num = int64(v)
} else if k == "item_type" {
item.ItemType = int(v)
}
}
items = append(items, item)
} }
items = append(items, item)
} }
parsedValue = items parsedValue = items

View File

@ -15,6 +15,7 @@ type ProjectService struct {
} }
func NewProjectService(db *gorm.DB) *ProjectService { func NewProjectService(db *gorm.DB) *ProjectService {
repo.ServerRepoInstance = repo.NewServerRepo(db)
return &ProjectService{repo: repo.NewProjectRepo(db)} return &ProjectService{repo: repo.NewProjectRepo(db)}
} }

View File

@ -9,10 +9,12 @@ import (
var projectsResourceHookMgr = map[string]map[string]any{ var projectsResourceHookMgr = map[string]map[string]any{
// 神魔大陆项目注册的资源钩子回调 // 神魔大陆项目注册的资源钩子回调
consts.RegisteredProjectId_shenmodalu: { consts.RegisteredProjectId_shenmodalu: {
consts.ResourcesName_Server: &smdl.ServerHook{}, // 查看了数据库所有数据之后还要连alisrv获取所有进程运行情况 consts.ResourcesName_Server: &smdl.ServerHook{}, // 查看了数据库所有数据之后还要连alisrv获取所有进程运行情况
consts.ResourcesName_Role: &smdl.RoleHook{}, // 所有角色走神魔大陆api直接获取 consts.ResourcesName_Role: &smdl.RoleHook{}, // 所有角色走神魔大陆api直接获取
consts.ResourcesName_Account: &smdl.AccountHook{}, // 所有角色走神魔大陆api直接获取 consts.ResourcesName_Account: &smdl.AccountHook{}, // 所有角色走神魔大陆api直接获取
consts.ResourcesName_Ban: &smdl.BanHook{}, // 所有角色走神魔大陆api直接获取 consts.ResourcesName_Ban: &smdl.BanHook{}, // 所有角色走神魔大陆api直接获取
consts.ResourcesName_MailGlobal: &smdl.MailGlobalHook{}, // 所有角色走神魔大陆api直接获取
consts.ResourcesName_MailRole: &smdl.MailRoleHook{}, // 所有角色走神魔大陆api直接获取
}, },
} }

View File

@ -6,6 +6,8 @@ import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/errcode" "admin/internal/errcode"
"admin/lib/httpclient" "admin/lib/httpclient"
"admin/lib/utils"
"math"
"strconv" "strconv"
) )
@ -24,9 +26,9 @@ func (hook *AccountHook) List(projectInfo *entity.Project, resource string, page
Msg string `json:"msg"` Msg string `json:"msg"`
Data struct { Data struct {
Data []struct { Data []struct {
Account string `json:"account"` Account string `json:"account"`
ServerId int `json:"serverId"` ServerId int `json:"serverId"`
RoleList []*model.Role `json:"roleList"` RoleList []*Role `json:"roleList"`
} `json:"data"` } `json:"data"`
} `json:"data"` } `json:"data"`
} }
@ -43,9 +45,19 @@ func (hook *AccountHook) List(projectInfo *entity.Project, resource string, page
Account: user.Account, Account: user.Account,
ServerConfId: strconv.Itoa(user.ServerId), ServerConfId: strconv.Itoa(user.ServerId),
} }
latestLoginTime := int64(0)
earliestCreateTime := int64(math.MaxInt64)
for _, v := range user.RoleList { for _, v := range user.RoleList {
userPo.RoleIds = append(userPo.RoleIds, v.RoleId) userPo.RoleIds = append(userPo.RoleIds, v.RoleId)
if latestLoginTime < v.LatestLoginTime {
latestLoginTime = v.LatestLoginTime
}
if earliestCreateTime > v.CreateTime {
earliestCreateTime = v.CreateTime
}
} }
userPo.LatestLoginTime = utils.ParseUnixTimestamp2LocalTime(latestLoginTime)
userPo.CreateTime = utils.ParseUnixTimestamp2LocalTime(earliestCreateTime)
et := (&entity.CommonResource{}).FromPo(userPo) et := (&entity.CommonResource{}).FromPo(userPo)
rows = append(rows, et.ToCommonDto()) rows = append(rows, et.ToCommonDto())
} }

View File

@ -28,13 +28,15 @@ func (hook *BanHook) Create(projectInfo *entity.Project, resource string, dtoObj
params.Add("server", banInfo.ServerConfID) params.Add("server", banInfo.ServerConfID)
params.Add("roleid", roleId) params.Add("roleid", roleId)
if banInfo.ExpireAt <= 0 { expireAt := banInfo.ExpireAt.Unix()
if expireAt <= 0 {
// 解封 // 解封
params.Add("forbidtime", "0") params.Add("forbidtime", "0")
params.Add("desc", "待写原因") params.Add("desc", "待写原因")
params.Add("desc", "你被永久封禁了") params.Add("desc", "你被永久封禁了")
} else { } else {
dura := banInfo.ExpireAt - time.Now().Unix() dura := expireAt - time.Now().Unix()
if dura <= 0 { if dura <= 0 {
// 解封 // 解封
params.Add("forbidtime", "0") params.Add("forbidtime", "0")

View File

@ -2,10 +2,12 @@ package smdl
import ( import (
"admin/apps/game/domain/entity" "admin/apps/game/domain/entity"
"admin/apps/game/domain/repo"
"admin/apps/game/model" "admin/apps/game/model"
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/errcode" "admin/internal/errcode"
"admin/lib/httpclient" "admin/lib/httpclient"
"admin/lib/xlog"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@ -45,7 +47,15 @@ func (hook *MailGlobalHook) Create(projectInfo *entity.Project, resource string,
serverIdsLen := len(mailInfo.ServerIDs) serverIdsLen := len(mailInfo.ServerIDs)
switch { switch {
case serverIdsLen == 0: case serverIdsLen == 0:
// todo 所有服 // 为空就发给所有服
serverList, err := repo.ServerRepoInstance.List(projectInfo.ProjectPo.ProjectId)
if err != nil {
xlog.Errorf("list all server by project:%v error:%v", projectInfo.ProjectPo.ProjectId, err)
return errcode.New(errcode.ServerError, "list all server by project:%v error:%v", projectInfo.ProjectPo.ProjectId, err)
}
for _, v := range serverList {
serverIds = append(serverIds, v.ToPo().(*model.Server).ServerConfID)
}
case serverIdsLen > 0: case serverIdsLen > 0:
for _, v := range mailInfo.ServerIDs { for _, v := range mailInfo.ServerIDs {
serverIds = append(serverIds, v) serverIds = append(serverIds, v)
@ -60,8 +70,16 @@ func (hook *MailGlobalHook) Create(projectInfo *entity.Project, resource string,
// 通知对应服务器 // 通知对应服务器
for _, serverId := range serverIds { for _, serverId := range serverIds {
serverIdInt, _ := strconv.Atoi(serverId)
if serverIdInt == 0 || serverIdInt%10000 > 500 {
// 过滤掉20801 28802这些跨服
continue
}
params.Set("server", serverId) params.Set("server", serverId)
xlog.Debugf("通知神魔大陆区服%v添加邮件:%v", serverId, params.Encode())
rsp := make(map[string]any) rsp := make(map[string]any)
err := httpclient.Request(alisrvAddr+"/mail_global", "get", params, &rsp) err := httpclient.Request(alisrvAddr+"/mail_global", "get", params, &rsp)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/errcode" "admin/internal/errcode"
"admin/lib/httpclient" "admin/lib/httpclient"
"admin/lib/xlog"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@ -49,6 +50,8 @@ func (hook *MailRoleHook) Create(projectInfo *entity.Project, resource string, d
// 通知对应服务器 // 通知对应服务器
for _, roleId := range mailInfo.RoleIDs { for _, roleId := range mailInfo.RoleIDs {
params.Set("playerid", roleId)
xlog.Debugf("发送神魔大陆个人邮件给角色:%v 内容:%v", roleId, params.Encode())
rsp := make(map[string]any) rsp := make(map[string]any)
err := httpclient.Request(alisrvAddr+"/mail_role", "get", params, &rsp) err := httpclient.Request(alisrvAddr+"/mail_role", "get", params, &rsp)
if err != nil { if err != nil {

View File

@ -6,8 +6,21 @@ import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/errcode" "admin/internal/errcode"
"admin/lib/httpclient" "admin/lib/httpclient"
"admin/lib/utils"
) )
type Role struct {
RoleId string `json:"roleId"`
Account string `json:"account"`
ServerConfId string `json:"serverId"`
Name string `json:"roleName"`
Status string `json:"status"`
Level int `json:"roleLevel"`
Profession string `json:"profession"`
LatestLoginTime int64 `json:"latestLoginAt"`
CreateTime int64 `json:"createAt"`
}
type RoleHook struct { type RoleHook struct {
} }
@ -22,7 +35,7 @@ func (hook *RoleHook) List(projectInfo *entity.Project, resource string, pageNo,
Code int `json:"code"` Code int `json:"code"`
Msg string `json:"msg"` Msg string `json:"msg"`
Data struct { Data struct {
Data []*model.Role `json:"data"` Data []*Role `json:"data"`
} `json:"data"` } `json:"data"`
} }
@ -33,7 +46,18 @@ func (hook *RoleHook) List(projectInfo *entity.Project, resource string, pageNo,
} }
for _, role := range rsp.Data.Data { for _, role := range rsp.Data.Data {
et := (&entity.CommonResource{}).FromPo(role) rolePo := &model.Role{
RoleId: role.RoleId,
Account: role.Account,
ServerConfId: role.ServerConfId,
Name: role.Name,
Status: role.Status,
Level: role.Level,
Profession: role.Profession,
LatestLoginTime: utils.ParseUnixTimestamp2LocalTime(role.LatestLoginTime),
CreateTime: utils.ParseUnixTimestamp2LocalTime(role.CreateTime),
}
et := (&entity.CommonResource{}).FromPo(rolePo)
rows = append(rows, et.ToCommonDto()) rows = append(rows, et.ToCommonDto())
} }

View File

@ -25,8 +25,9 @@ func (hook *ServerHook) List(projectInfo *entity.Project, resource string, pageN
HelpText: "进程运行状态:未知、运行中、停止", HelpText: "进程运行状态:未知、运行中、停止",
Readonly: false, Readonly: false,
Required: false, Required: false,
Choices: nil, Choices: make([]*dto.CommonDtoFieldChoice, 0),
MultiChoice: false, MultiChoice: false,
Uneditable: true,
}) })
type ServerInfo struct { type ServerInfo struct {

View File

@ -0,0 +1,36 @@
package repo
import (
"admin/apps/game/domain/entity"
"admin/apps/game/model"
"admin/internal/errcode"
"gorm.io/gorm"
)
var ServerRepoInstance IServerRepo
type IServerRepo interface {
List(projectId string) ([]*entity.CommonResource, error)
}
func NewServerRepo(db *gorm.DB) IServerRepo {
return &serverRepoImpl{db: db}
}
type serverRepoImpl struct {
db *gorm.DB
}
func (impl *serverRepoImpl) List(projectId string) ([]*entity.CommonResource, error) {
list := make([]*model.Server, 0)
err := impl.db.Where("project_id = ?", projectId).Find(&list).Error
if err != nil {
return nil, errcode.New(errcode.DBError, "list server error:%v", err)
}
list1 := make([]*entity.CommonResource, 0, len(list))
for _, v := range list {
list1 = append(list1, (&entity.CommonResource{}).FromPo(v))
}
return list1, nil
}

View File

@ -2,6 +2,7 @@ package model
import ( import (
"admin/internal/db" "admin/internal/db"
"time"
) )
func init() { func init() {
@ -10,11 +11,11 @@ func init() {
// Account 空表就是用来兼容资源增删改查公共操作的实际列举账号等都是走各个项目api拉取 // Account 空表就是用来兼容资源增删改查公共操作的实际列举账号等都是走各个项目api拉取
type Account struct { type Account struct {
Account string `name:"账号" json:"account"` Account string `name:"账号" json:"account"`
ServerConfId string `name:"区服id" json:"serveId"` ServerConfId string `name:"区服id" json:"serveId"`
RoleIds []string `gorm:"type:json;serializer:json" name:"角色id列表" json:"roleIds"` RoleIds []string `gorm:"type:json;serializer:json" name:"角色id列表" json:"roleIds"`
LatestLoginTime int64 `name:"最近登录时间" json:"latest_login_time"` LatestLoginTime time.Time `name:"最近登录时间" json:"latest_login_time"`
CreateTime int64 `name:"创建时间" json:"create_time"` CreateTime time.Time `name:"创建时间" json:"create_time"`
} }
func (lm *Account) TableName() string { func (lm *Account) TableName() string {

View File

@ -3,7 +3,6 @@ package model
import ( import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -12,16 +11,15 @@ func init() {
} }
type Ban struct { type Ban struct {
ID int `gorm:"primarykey" readonly:"true"` ID int `gorm:"primarykey" readonly:"true"`
ProjectId string `gorm:"type:varchar(255);uniqueIndex:idx_ban"` ProjectId string `gorm:"type:varchar(255);uniqueIndex:idx_ban"`
ServerConfID string `gorm:"type:varchar(200);uniqueIndex:idx_server" required:"true"` ServerConfID string `gorm:"type:varchar(200);uniqueIndex:idx_server" name:"区服id" required:"true" choices:"GetServerConfIDChoices" uneditable:"true"`
BanType string `name:"封禁类型" required:"true" choices:"GetBanTypeChoices" multi_choice:"true"` BanType string `name:"封禁类型" required:"true" choices:"GetBanTypeChoices" uneditable:"true"`
Value string `gorm:"type:varchar(128);uniqueIndex:idx_ban" name:"封禁账号" required:"true" desc:"填账号"` Value string `gorm:"type:varchar(128);uniqueIndex:idx_ban" name:"封禁值" required:"true" desc:"根据封禁类型填对应值例如ip就填ip地址" uneditable:"true"`
ExpireAt int64 `name:"封禁到期时间" desc:"封禁到期时间0表示永久封禁"` BanReason string `name:"封禁理由" desc:"封禁理由,会推送给客户端弹窗" required:"true"`
ExpireAt time.Time `name:"封禁到期时间" desc:"封禁到期时间0表示永久封禁"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *Ban) TableName() string { func (lm *Ban) TableName() string {
@ -32,15 +30,35 @@ func (m *Ban) GetId() int {
return m.ID return m.ID
} }
func (m *Ban) GetServerConfIDChoices(projectId string) []*dto.CommonDtoFieldChoice {
return getChoiceServers(projectId)
}
func (m *Ban) GetBanTypeChoices(projectId string) []*dto.CommonDtoFieldChoice { func (m *Ban) GetBanTypeChoices(projectId string) []*dto.CommonDtoFieldChoice {
return []*dto.CommonDtoFieldChoice{ return []*dto.CommonDtoFieldChoice{
{ {
Desc: "作弊", Desc: "账号",
Value: "作弊", Value: "account",
}, },
{ {
Desc: "广告", Desc: "角色",
Value: "广告", Value: "role",
},
{
Desc: "IP",
Value: "ip",
},
{
Desc: "账号发言",
Value: "account_chat",
},
{
Desc: "角色发言",
Value: "role_chat",
},
{
Desc: "设备号",
Value: "device_id",
}, },
} }
} }

View File

@ -2,7 +2,6 @@ package model
import ( import (
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -17,9 +16,7 @@ type DevicePush struct {
Title string `name:"通知标题" desc:""` Title string `name:"通知标题" desc:""`
Content string `name:"通知内容"` Content string `name:"通知内容"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *DevicePush) TableName() string { func (lm *DevicePush) TableName() string {

View File

@ -36,6 +36,7 @@ type CommonDtoFieldDesc struct {
Required bool `json:"required"` // 是否必填,不能为空 Required bool `json:"required"` // 是否必填,不能为空
Choices []*CommonDtoFieldChoice `json:"choices"` // 可选项,用于字段做下拉框 Choices []*CommonDtoFieldChoice `json:"choices"` // 可选项,用于字段做下拉框
MultiChoice bool `json:"multi_choice"` // 是否多选 MultiChoice bool `json:"multi_choice"` // 是否多选
Uneditable bool `json:"uneditable"` // 不可编辑某些数据一旦新增之后不能修改例如封禁的值、服务器的id等
} }
//type CommonDtoValue struct { //type CommonDtoValue struct {

View File

@ -3,7 +3,6 @@ package model
import ( import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -21,9 +20,7 @@ type GlobalMail struct {
Content string `name:"邮件内容" required:"true"` Content string `name:"邮件内容" required:"true"`
Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" choices:"GetChoiceItems"` Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" choices:"GetChoiceItems"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *GlobalMail) TableName() string { func (lm *GlobalMail) TableName() string {

View File

@ -11,8 +11,8 @@ type IModel interface {
var GetProjectServersHandler func(projectId string) ([]*Server, error) var GetProjectServersHandler func(projectId string) ([]*Server, error)
func getChoiceServers(args ...any) []*dto.CommonDtoFieldChoice { func getChoiceServers(projectId string) []*dto.CommonDtoFieldChoice {
servers, err := GetProjectServersHandler(args[0].(string)) servers, err := GetProjectServersHandler(projectId)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -3,7 +3,6 @@ package model
import ( import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -14,14 +13,12 @@ func init() {
type Notice struct { type Notice struct {
ID int `gorm:"primarykey" readonly:"true"` ID int `gorm:"primarykey" readonly:"true"`
ProjectId string ProjectId string
ServerIDs []int `gorm:"type:json;serializer:json" name:"公告生效服务器" desc:"为空表示所有服" choices:"GetChoiceServers"` ServerIDs []int `gorm:"type:json;serializer:json" name:"公告生效服务器" desc:"为空表示所有服" choices:"GetChoiceServers"`
Content string `name:"公告内容" required:"true"` Content string `name:"公告内容" required:"true"`
StartAt int64 `name:"开始时间" required:"true"` StartAt time.Time `name:"开始时间" required:"true"`
EndAt int64 `name:"结束时间" required:"true"` EndAt time.Time `name:"结束时间" required:"true"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *Notice) TableName() string { func (lm *Notice) TableName() string {

View File

@ -2,7 +2,6 @@ package model
import ( import (
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -14,7 +13,7 @@ func init() {
type Project struct { type Project struct {
ID int `gorm:"primarykey" readonly:"true"` ID int `gorm:"primarykey" readonly:"true"`
ProjectId string `gorm:"type:varchar(255);unique" name:"项目id" readonly:"true"` ProjectId string `gorm:"type:varchar(255);unique" name:"项目id" readonly:"true"`
Name string `gorm:"primarykey" required:"true" name:"项目名" readonly:"true"` Name string `gorm:"primarykey" required:"true" name:"项目名" readonly:"true" uneditable:"true"`
Desc string `name:"项目描述"` Desc string `name:"项目描述"`
// command_list接口服务器地址为空代表由由项目下各个逻辑服提供command_list. // command_list接口服务器地址为空代表由由项目下各个逻辑服提供command_list.
// 取决于每个项目改造难度: // 取决于每个项目改造难度:
@ -23,9 +22,7 @@ type Project struct {
// 调用各个逻辑服执行,本后台执行指令需要根据逻辑服数量调用; // 调用各个逻辑服执行,本后台执行指令需要根据逻辑服数量调用;
ApiAddr string `name:"游戏api地址" desc:"api服务器地址例如神魔大陆就是alisrv服务器地址用于后台调用gm"` ApiAddr string `name:"游戏api地址" desc:"api服务器地址例如神魔大陆就是alisrv服务器地址用于后台调用gm"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *Project) TableName() string { func (lm *Project) TableName() string {

View File

@ -2,7 +2,6 @@ package model
import ( import (
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -16,9 +15,7 @@ type RewardCode struct {
Group int `name:"奖励码组"` Group int `name:"奖励码组"`
Code int `name:"奖励码"` Code int `name:"奖励码"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *RewardCode) TableName() string { func (lm *RewardCode) TableName() string {

View File

@ -2,6 +2,7 @@ package model
import ( import (
"admin/internal/db" "admin/internal/db"
"time"
) )
func init() { func init() {
@ -10,15 +11,15 @@ func init() {
// Role 空表就是用来兼容资源增删改查公共操作的实际列举账号等都是走各个项目api拉取 // Role 空表就是用来兼容资源增删改查公共操作的实际列举账号等都是走各个项目api拉取
type Role struct { type Role struct {
RoleId string `name:"账号" json:"roleId"` RoleId string `name:"角色ID" json:"roleId"`
Account string `name:"账号" json:"account"` Account string `name:"账号" json:"account"`
ServerConfId string `name:"区服id" json:"serverId"` ServerConfId string `name:"区服id" json:"serverId"`
Name string `name:"名称" json:"roleName"` Name string `name:"名称" json:"roleName"`
Status string `name:"状态" desc:"离线|在线" json:"status"` Status string `name:"状态" desc:"离线|在线" json:"status"`
Level int `name:"等级" json:"roleLevel"` Level int `name:"等级" json:"roleLevel"`
Profession string `name:"职业" json:"profession"` Profession string `name:"职业" json:"profession"`
LatestLoginTime int64 `name:"最近登录时间" json:"latestLoginAt"` LatestLoginTime time.Time `name:"最近登录时间" json:"latestLoginAt"`
CreateTime int64 `name:"创建时间" json:"createAt"` CreateTime time.Time `name:"创建时间" json:"createAt"`
} }
func (lm *Role) TableName() string { func (lm *Role) TableName() string {

View File

@ -3,7 +3,6 @@ package model
import ( import (
"admin/apps/game/model/dto" "admin/apps/game/model/dto"
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -20,15 +19,13 @@ type MailAttachItem struct {
type RoleMail struct { type RoleMail struct {
ID int `gorm:"primarykey" readonly:"true"` ID int `gorm:"primarykey" readonly:"true"`
ProjectId string ProjectId string
RoleIDs []string `gorm:"type:json;serializer:json" name:"生效的角色id" required:"true"` RoleIDs []string `gorm:"type:json;serializer:json" name:"生效的角色id" desc:"生效的角色id逗号分隔多个" required:"true"`
ServerID string `name:"所属区服" choices:"GetChoiceServers" multi_choice:"false"` ServerID string `name:"所属区服" choices:"GetChoiceServers" required:"true"`
Title string `name:"邮件标题" required:"true"` Title string `name:"邮件标题" required:"true"`
Content string `name:"邮件内容" required:"true"` Content string `name:"邮件内容" required:"true"`
Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" choices:"GetChoiceItems"` Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" choices:"GetChoiceItems"`
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *RoleMail) TableName() string { func (lm *RoleMail) TableName() string {

View File

@ -2,7 +2,6 @@ package model
import ( import (
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -14,18 +13,16 @@ func init() {
type Server struct { type Server struct {
ID int `gorm:"primarykey" readonly:"true"` ID int `gorm:"primarykey" readonly:"true"`
ProjectId string `gorm:"type:varchar(200);uniqueIndex:idx_server"` ProjectId string `gorm:"type:varchar(200);uniqueIndex:idx_server"`
ServerConfID string `gorm:"type:varchar(200);uniqueIndex:idx_server" required:"true"` ServerConfID string `gorm:"type:varchar(200);uniqueIndex:idx_server" name:"区服id" required:"true" uneditable:"true"`
Desc string Desc string `name:"描述"`
// command_list接口服务器地址为空代表由由项目统一提供command_list. // command_list接口服务器地址为空代表由由项目统一提供command_list.
// 取决于每个项目改造难度: // 取决于每个项目改造难度:
// 为空就代表项目要实现一个自己统一对外暴露的gm调用服务对内聚合、分发指令执行本后台执行指令只调用一次 // 为空就代表项目要实现一个自己统一对外暴露的gm调用服务对内聚合、分发指令执行本后台执行指令只调用一次
// 不为空就代表command_list实现在各个逻辑服由本后台系统在执行指令时聚合、分发指令 // 不为空就代表command_list实现在各个逻辑服由本后台系统在执行指令时聚合、分发指令
// 调用各个逻辑服执行,本后台执行指令需要根据逻辑服数量调用; // 调用各个逻辑服执行,本后台执行指令需要根据逻辑服数量调用;
ApiAddr string //ApiAddr string
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *Server) TableName() string { func (lm *Server) TableName() string {

View File

@ -1,8 +1,8 @@
package model package model
import ( import (
"admin/apps/game/model/dto"
"admin/internal/db" "admin/internal/db"
"gorm.io/gorm"
"time" "time"
) )
@ -13,14 +13,11 @@ func init() {
type WhiteList struct { type WhiteList struct {
ID int `gorm:"primarykey" readonly:"true"` ID int `gorm:"primarykey" readonly:"true"`
ProjectId string `gorm:"type:varchar(256);uniqueIndex:idx_whitelist"` ProjectId string `gorm:"type:varchar(256);uniqueIndex:idx_whitelist"`
ServerConfID string `gorm:"type:varchar(200);uniqueIndex:idx_server" required:"true"` ServerConfID string `gorm:"type:varchar(200);uniqueIndex:idx_server" name:"区服id" required:"true" choices:"GetChoiceServers"`
Account string `gorm:"type:varchar(128);uniqueIndex:idx_whitelist"` Account string `gorm:"type:varchar(128);uniqueIndex:idx_whitelist" name:"账户" required:"true"`
AccountType int `gorm:"uniqueIndex:idx_whitelist"` Desc string `name:"描述"`
Desc string
CreatedAt time.Time `readonly:"true"` CreatedAt time.Time `readonly:"true"`
UpdatedAt time.Time `readonly:"true"`
DeletedAt gorm.DeletedAt `gorm:"index" readonly:"true"`
} }
func (lm *WhiteList) TableName() string { func (lm *WhiteList) TableName() string {
@ -30,3 +27,7 @@ func (lm *WhiteList) TableName() string {
func (m *WhiteList) GetId() int { func (m *WhiteList) GetId() int {
return m.ID return m.ID
} }
func (m *WhiteList) GetChoiceServers(projectId string) []*dto.CommonDtoFieldChoice {
return getChoiceServers(projectId)
}

44
admin/lib/utils/time.go Normal file
View File

@ -0,0 +1,44 @@
package utils
import (
"fmt"
"time"
)
func ParseUnixTimestamp2LocalTime(ts int64) (local time.Time) {
sec := int64(0)
nsec := int64(0)
switch {
case ts >= 1e18: // 纳秒
sec = ts / int64(1e9)
nsec = ts % int64(1e9)
case ts >= 1e15: // 微妙
sec = ts / int64(1e6)
nsec = ts % int64(1e6)
case ts >= 1e12: // 毫秒
sec = ts / int64(1e3)
nsec = ts % int64(1e3)
case ts >= 1e9: // 秒
sec = ts
nsec = 0
default:
panic(fmt.Errorf("ts:%v invalid parse to local time"))
}
t := time.Unix(sec, nsec)
ts2str := t.Format("2006-01-02 15:04:05.000000000")
local, _ = time.ParseInLocation("2006-01-02 15:04:05.000000000", ts2str, time.Local)
return local
}
// GetTimeZoneLocalTime 获取指定时区的当地时间
func GetTimeZoneLocalTime(timeZone int) time.Time {
offsetZone := time.FixedZone(time.Local.String(), timeZone*60*60)
return time.Now().In(offsetZone)
}
// GetTimeZoneLocalTimeByTimestamp 获取指定时间戳与时区的当地时间
func GetTimeZoneLocalTimeByTimestamp(ts int64, timeZone int) time.Time {
offsetZone := time.FixedZone(time.Local.String(), timeZone*60*60)
return time.Unix(ts, 0).Local().In(offsetZone)
}

View File

@ -1,4 +1,29 @@
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family: Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.el-table { .el-table {
.el-table__header-wrapper, .el-table__fixed-header-wrapper { .el-table__header-wrapper, .el-table__fixed-header-wrapper {
th { th {
@ -32,4 +57,4 @@
.el-table .el-dropdown, .el-icon-arrow-down { .el-table .el-dropdown, .el-icon-arrow-down {
font-size: 12px; font-size: 12px;
} }

View File

@ -39,11 +39,13 @@ const listData = async () => {
for (let i = 0; i < fieldsDescInfo.value.length; i++) { for (let i = 0; i < fieldsDescInfo.value.length; i++) {
var field = fieldsDescInfo.value[i] var field = fieldsDescInfo.value[i]
dialogAddForm.value[field.key] = '' dialogAddForm.value[field.key] = ''
if (field.required == true) { if (field.required == true) {
rules.value[field.key] = [{required: true, message: field.name + "不能为空", trigger: "blur"}] rules.value[field.key] = [{required: true, message: field.name + "不能为空", trigger: "blur"}]
} }
if (field.type == "items") { if (field.type == "items") {
dialogAddForm.value[field.key] = []
for (let j = 0; j < rows.value.length; j++) { for (let j = 0; j < rows.value.length; j++) {
rows.value[j].jsonValue = JSON.stringify(rows.value[j][field.key]) rows.value[j].jsonValue = JSON.stringify(rows.value[j][field.key])
} }
@ -71,6 +73,7 @@ const dialogEditFormRef = ref(null)
const dialogAddForm = ref({ const dialogAddForm = ref({
ServerIDs: [], ServerIDs: [],
Attach: [],
}) })
const dialogEditForm = ref({}) const dialogEditForm = ref({})
@ -90,6 +93,7 @@ const submitAdd = async () => {
}) })
rows.value.push(res.data.dto) rows.value.push(res.data.dto)
dialogAddVisible.value = false dialogAddVisible.value = false
handleCloseDialog()
}, (err) => { }, (err) => {
console.log("添加报错:", err) console.log("添加报错:", err)
}) })
@ -115,6 +119,7 @@ const submitEdit = async () => {
}) })
dialogEditVisible.value = false dialogEditVisible.value = false
rows.value[dialogEditForm.value.oldIndex] = res.data.dto rows.value[dialogEditForm.value.oldIndex] = res.data.dto
handleCloseDialog()
}, (err) => { }, (err) => {
console.log("添加报错:", err) console.log("添加报错:", err)
}) })
@ -135,18 +140,22 @@ const handleEdit = (index, row) => {
} }
const handleDelete = (index, row) => { const handleDelete = (index, row) => {
resourceDelete(resource_url, {id: row.ID}).then((res) => { ElMessageBox.confirm("确定要删除吗?").then(() => {
ElNotification({ resourceDelete(resource_url, {id: row.ID}).then((res) => {
title: "删除结果通知", ElNotification({
message: "删除指令服务器[" + row.name + "]成功!", title: "删除结果通知",
type: 'success', message: "删除数据[" + row.ID + "]成功!",
duration: 4000, type: 'success',
"show-close": true, duration: 4000,
"show-close": true,
})
rows.value.splice(index, 1)
}, (err) => {
console.log("delet error:", err)
}) })
rows.value.splice(index, 1) }).catch(() => {
}, (err) => {
console.log("delet error:", err)
}) })
} }
function addItem(fieldDescInfo) { function addItem(fieldDescInfo) {
@ -184,6 +193,7 @@ function deleteItem(row) {
} }
const handleCloseDialog = () => { const handleCloseDialog = () => {
console.log("关闭添加/编辑弹窗")
dialogAddVisible.value = false dialogAddVisible.value = false
dialogEditVisible.value = false dialogEditVisible.value = false
dialogAddForm.value = { dialogAddForm.value = {
@ -213,24 +223,6 @@ const handleCloseDialog = () => {
<el-table-column prop="func" label="功 能"> <el-table-column prop="func" label="功 能">
<template #default="scope"> <template #default="scope">
<!-- <el-button size="default" type="primary"-->
<!-- @click="tableRowClick(row_click_btn1, scope.$index, scope.row)"-->
<!-- v-if="(row_click_btn1.text !== '')">-->
<!-- <el-icon style="vertical-align: middle">-->
<!-- <component :is="row_click_btn1.icon"></component>-->
<!-- </el-icon>-->
<!-- <span>{{ row_click_btn1.text }}</span>-->
<!-- </el-button>-->
<!-- <el-button size="default" type="primary"-->
<!-- @click="tableRowClick(row_click_btn2, scope.$index, scope.row)"-->
<!-- v-if="(row_click_btn2.text !== '')">-->
<!-- <el-icon style="vertical-align: middle">-->
<!-- <component :is="row_click_btn2.icon"></component>-->
<!-- </el-icon>-->
<!-- <span>{{ row_click_btn2.text }}</span>-->
<!-- </el-button>-->
<el-button size="default" type="success" @click="handleEdit( scope.$index, scope.row)" <el-button size="default" type="success" @click="handleEdit( scope.$index, scope.row)"
v-if="(resource_raw_node.meta.methods.put === true)"> v-if="(resource_raw_node.meta.methods.put === true)">
<el-icon style="vertical-align: middle"> <el-icon style="vertical-align: middle">
@ -251,15 +243,97 @@ const handleCloseDialog = () => {
<el-dialog v-model="dialogAddVisible" :mask="true" title="添加" :modal="true" :before-close="handleCloseDialog" <el-dialog v-model="dialogAddVisible" :mask="true" title="添加" :modal="true" :before-close="handleCloseDialog"
destroy-on-close> destroy-on-close>
<el-form ref="dialogAddFormRef" :model="dialogAddForm" :rules="rules"> <el-form ref="dialogAddFormRef" :model="dialogAddForm" :rules="rules" label-position="right"
label-width="130px">
<template v-for="fieldDescInfo in fieldsDescInfo"> <template v-for="fieldDescInfo in fieldsDescInfo">
<!--如何是items类型就是物品下拉框+道具组合--> <!--如何是items类型就是物品下拉框+道具组合-->
<!--如何是[]string类型就是下拉框或多选框-->
<template v-if="(fieldDescInfo.type === 'items')"> <template v-if="(fieldDescInfo.type === 'items')">
<el-form :inline="true" :model="item"> <el-form :inline="true" :model="item" label-position="right">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key" label-width="130px">
<el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start">
<el-select placeholder="--选择道具后填数量点击添加--" v-model="item.id" style="width: 150px"
filterable>
<el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc"
:value="info.value"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
<el-form-item label="数量" prop="num">
<el-input type="number" v-model="item.num" placeholder="请输入数量" style="width: 150px"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addItem(fieldDescInfo)">添加</el-button>
</el-form-item>
</el-form>
<el-form-item label="奖励列表" prop="Attach">
<el-table :data="dialogAddForm.Attach" border>
<el-table-column label="道具id" prop="id"/>
<el-table-column label="数量" prop="num"/>
<el-table-column label="操作">
<template #default="scope">
<el-button type="danger" size="small" @click="deleteItem(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</template>
<template v-else-if="(fieldDescInfo.readonly !== true)">
<!-- 有可选项的字段走下拉框或者多选框 -->
<template v-if="(fieldDescInfo.choices !== undefined && fieldDescInfo.choices.length > 0)">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key"> <el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start"> <el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start">
<el-select placeholder="可选项" v-model="item.id" style="width: 150px"> <el-select :placeholder="(fieldDescInfo.multi_choice === true ? '--多选--' : '--单选--')"
v-model="dialogAddForm[fieldDescInfo.key]" style="width: 150px"
:multiple="(fieldDescInfo.multi_choice === true)">
<el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc"
:value="info.value"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</template>
<!-- 时间戳字段展示时间选择器 -->
<template v-else-if="(fieldDescInfo.type === 'Time')">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-date-picker v-model="dialogAddForm[fieldDescInfo.key]" type="datetime"
placeholder="选个时间" format="YYYY/MM/DD HH:mm:ss"
value-format="YYYY/MM/DD HH:mm:ss"></el-date-picker>
</el-form-item>
</template>
<!-- 否则就是普通字段 -->
<template v-else>
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-input v-model="dialogAddForm[fieldDescInfo.key]"
:placeholder="fieldDescInfo.help_text"></el-input>
</el-form-item>
</template>
</template>
</template>
<el-form-item>
<el-button @click="submitAdd(dialogAddFormRef)" size="large" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog v-model="dialogEditVisible" :mask="true" title="编辑" :modal="true" :before-close="handleCloseDialog"
destroy-on-close>
<el-form ref="dialogEditFormRef" :model="dialogEditForm" :rules="rules" class="operation_form"
label-width="130px">
<template v-for="fieldDescInfo in fieldsDescInfo">
<!--如果是items类型就是物品下拉框+道具组合-->
<template v-if="(fieldDescInfo.type === 'items')">
<el-form :inline="true" :model="item" label-position="right"
label-width="130px">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start">
<el-select placeholder="--选择道具后填数量点击添加--" v-model="item.id" style="width: 150px"
filterable>
<el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc" <el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc"
:value="info.value"></el-option> :value="info.value"></el-option>
</el-select> </el-select>
@ -273,7 +347,7 @@ const handleCloseDialog = () => {
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-form-item label="奖励列表" prop="attachmentsList"> <el-form-item label="奖励列表" prop="attachmentsList">
<el-table :data="dialogAddForm.Attach" border> <el-table :data="dialogEditForm.Attach" border>
<el-table-column label="道具id" prop="id"/> <el-table-column label="道具id" prop="id"/>
<el-table-column label="数量" prop="num"/> <el-table-column label="数量" prop="num"/>
<el-table-column label="操作"> <el-table-column label="操作">
@ -285,50 +359,54 @@ const handleCloseDialog = () => {
</el-form-item> </el-form-item>
</template> </template>
<template v-else-if="(fieldDescInfo.type === '[]string')">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start">
<el-select placeholder="可选项" v-model="dialogAddForm.ServerIDs" style="width: 150px"
:multiple="(fieldDescInfo.multi_choice === true)">
<el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc"
:value="info.value"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</template>
<template v-else-if="(fieldDescInfo.readonly !== true)"> <template v-else-if="(fieldDescInfo.readonly !== true)">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key"> <template v-if="(fieldDescInfo.uneditable !== true)">
<el-input v-model="dialogAddForm[fieldDescInfo.key]" :placeholder="fieldDescInfo.name"></el-input> <!-- 有可选项的字段走下拉框或者多选框 -->
</el-form-item> <template v-if="(fieldDescInfo.choices !== undefined && fieldDescInfo.choices.length > 0)">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start">
<el-select :placeholder="(fieldDescInfo.multi_choice === true ? '--多选--' : '--单选--')"
v-model="dialogEditForm[fieldDescInfo.key]" style="width: 150px"
:multiple="(fieldDescInfo.multi_choice === true)">
<el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc"
:value="info.value"></el-option>
</el-select>
</el-tooltip>
</el-form-item>
</template>
<!-- 时间戳字段展示时间选择器 -->
<template v-else-if="(fieldDescInfo.type === 'Time')">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-date-picker v-model="dialogEditForm[fieldDescInfo.key]" type="datetime"
placeholder="选个时间" format="YYYY/MM/DD HH:mm:ss"
value-format="YYYY/MM/DD HH:mm:ss"></el-date-picker>
</el-form-item>
</template>
<!-- 否则就是普通字段 -->
<template v-else>
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-input v-model="dialogEditForm[fieldDescInfo.key]"
:placeholder="fieldDescInfo.help_text"></el-input>
</el-form-item>
</template>
</template>
<template v-else>
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-input v-model="dialogEditForm[fieldDescInfo.key]"
:placeholder="fieldDescInfo.help_text" disabled></el-input>
</el-form-item>
</template>
</template> </template>
<!-- <el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">--> <!-- <el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">-->
<!-- <el-tooltip effect="light" :content="fieldDescInfo.help_text" placement="bottom-start"--> <!-- <el-input v-model="dialogEditForm[fieldDescInfo.key]"></el-input>-->
<!-- v-if="(fieldDescInfo.type === 'items')">-->
<!-- <el-select placeholder="可选项">-->
<!-- <el-option v-for="info in fieldDescInfo.choices" :key="info.desc" :label="info.desc"-->
<!-- :value="info.value"></el-option>-->
<!-- </el-select>-->
<!-- </el-tooltip>-->
<!-- <el-input v-model="dialogAddForm[fieldDescInfo.key]" :placeholder="fieldDescInfo.name" v-else></el-input>-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
</template> </template>
<el-button @click="submitAdd(dialogAddFormRef)" size="large" type="primary">提交</el-button>
</el-form>
</el-dialog>
<el-dialog v-model="dialogEditVisible" :mask="true" title="编辑" :modal="true" :before-close="handleCloseDialog"
destroy-on-close>
<el-form ref="dialogEditFormRef" :model="dialogEditForm" :rules="rules">
<template v-for="fieldDescInfo in fieldsDescInfo">
<el-form-item :label="fieldDescInfo.name" :prop="fieldDescInfo.key">
<el-input v-model="dialogEditForm[fieldDescInfo.key]"></el-input>
</el-form-item>
</template>
<el-form-item> <el-form-item>
<el-button @click="submitEdit(dialogEditFormRef)" size="large" type="primary">提交</el-button> <el-button @click="submitEdit(dialogEditFormRef)" size="large" type="primary">提交</el-button>
</el-form-item> </el-form-item>

View File

@ -1,4 +1,4 @@
import './assets/main.css' // import './assets/main.css'
import {createApp} from 'vue' import {createApp} from 'vue'
import {createPinia} from 'pinia' import {createPinia} from 'pinia'

View File

@ -28,11 +28,12 @@ router.beforeEach((to, from, next) => {
projectList.value = res.data.projects projectList.value = res.data.projects
setProjectOperationRoutes(projectList.value) setProjectOperationRoutes(projectList.value)
// console.log("all routes:", router.getRoutes()) // console.log("all routes:", router.getRoutes())
next() next({...to, replace: true}); // 在已有页面里查找
}, (err) => { }, (err) => {
console.log("跳转路径:", to.path, " 报错:", err) console.log("跳转路径:", to.path, " 报错:", err)
}) })
} else { } else {
console.log("op tree routes length valid:", projectOpTreeRoutes.value.length)
next() next()
} }
} }

View File

@ -8,6 +8,7 @@ import LocalCache from "@/stores/project.js";
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
// //
const activeMenu = computed(() => route.path) const activeMenu = computed(() => route.path)
@ -22,49 +23,76 @@ const handleMenuSelect = (clickResource) => {
<template> <template>
<main> <main>
<div> <div>
<el-container style="height: 100vh;"> <el-container class="layout-container">
<el-aside class="el-aside-demo" width="200px"> <el-aside class="el-aside-demo">
<!-- <el-avatar shape="square" :size="100" :src="avatarUrl"></el-avatar>--> <div class="sidebar-content">
<div class="sidebar-logo"> <!-- <el-avatar shape="square" :size="100" :src="avatarUrl"></el-avatar>-->
<img src="@/assets/logo.svg" class="logo" alt="Logo"> <div class="sidebar-logo">
<span class="system-name">游戏后台管理系统</span> <img src="@/assets/logo.svg" class="logo" alt="Logo">
<span class="system-name">游戏后台管理系统</span>
</div>
<el-menu :default-active="activeMenu" class="el-menu-vertical-demo" :collapse="false">
<!-- 静态菜单 -->
<el-menu-item index="/user" @click="$router.push('/user')">
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/project" @click="$router.push('/project')">
<span>项目管理</span>
</el-menu-item>
<!-- 分割条 -->
<el-divider></el-divider>
<!-- 动态菜单每个项目操作 -->
<template v-for="project in projectOpTreeRoutes">
<el-sub-menu :index="project.path">
<!-- 设置菜单栏标题 -->
<template #title>
<span>{{ project.meta.projectName }}</span>
</template>
<!-- 添加项目资源操作点击菜单 -->
<el-menu-item v-for="resource in project.children" :key="resource.path" :index="resource.path"
@click="handleMenuSelect(resource)">
{{ resource.meta.desc }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</div> </div>
<el-menu :default-active="activeMenu" class="el-menu-vertical-demo" :collapse="isCollapse">
<!-- 静态菜单 -->
<el-menu-item index="/user" @click="$router.push('/user')">
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/project" @click="$router.push('/project')">
<span>项目管理</span>
</el-menu-item>
<!-- 分割条 -->
<el-divider></el-divider>
<!-- 动态菜单每个项目操作 -->
<template v-for="project in projectOpTreeRoutes">
<el-sub-menu :index="project.path">
<!-- 设置菜单栏标题 -->
<template #title>
<span>{{ project.meta.projectName }}</span>
</template>
<!-- 添加项目资源操作点击菜单 -->
<el-menu-item v-for="resource in project.children" :key="resource.path" :index="resource.path"
@click="handleMenuSelect(resource)">
{{ resource.meta.desc }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside> </el-aside>
<el-container class="el-container-right"> <el-container class="el-container-right">
<el-header style="border-bottom: #1f2d3d 1px solid">游戏后台管理系统</el-header> <el-header class="el-header-demo">
<el-main> <div class="header-content">
<div class="avatar-container">
<el-dropdown class="right-menu-item hover-effect" trigger="click">
<!-- 头像 -->
<div class="avatar-wrapper">
<img :src="avatarUrl" class="user-avatar"/>
<el-icon color="black" size="20">
<ArrowDownBold/>
</el-icon>
</div>
<!-- 头像操作下拉菜单 -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- <el-avatar shape="square" :size="100" :src="avatarUrl"></el-avatar>-->
</el-header>
<el-main class="el-main-demo">
<router-view :key="$route.fullPath"></router-view> <router-view :key="$route.fullPath"></router-view>
</el-main> </el-main>
</el-container> </el-container>
@ -73,22 +101,7 @@ const handleMenuSelect = (clickResource) => {
</main> </main>
</template> </template>
<style scoped> <style lang="scss" scoped>
.dashboard-container {
display: flex;
/* 使用flex布局 */
justify-content: space-between;
/* 子元素之间的间隔 */
align-items: flex-start;
/* 子元素垂直方向顶部对齐 */
padding: 20px;
max-width: 1200px;
margin: 0 auto;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
background-color: #71e4ae;
}
h1 { h1 {
color: #333; color: #333;
@ -96,80 +109,46 @@ h1 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.info-list { .layout-container {
list-style-type: none; height: 100vh;
/* 移除列表项前的标记 */ overflow: hidden;
padding: 0;
.el-aside-demo {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 200px;
z-index: 1000;
overflow-y: auto;
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.1);
background-color: #4d4f52;
.sidebar-content {
overflow-y: auto;
}
}
.el-container-right {
min-height: 100vh;
margin-left: 200px;
flex-direction: column;
.el-header-demo {
height: 60px;
background: #fff;
border-bottom: 1px solid #e6e6e6;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.el-main-demo {
flex: 1;
padding: 20px;
background: #f0f2f5;
}
}
} }
.info-list li {
margin-bottom: 10px;
font-size: 16px;
color: #666;
}
.sidebar {
width: 200px;
/* 侧边栏宽度 */
background-color: #f4f4f4;
/* 侧边栏背景色 */
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.sidebar ul {
list-style-type: none;
padding: 0;
}
.sidebar li {
margin-bottom: 10px;
}
.sidebar a {
color: #333;
text-decoration: none;
font-size: 16px;
display: block;
/* 使链接占据整行 */
padding: 8px;
border-radius: 4px;
transition: background-color 0.3s;
/* 平滑过渡效果 */
}
.sidebar a:hover {
background-color: #c6ec97;
/* 鼠标悬停时的背景色 */
}
.app-container {
display: flex;
}
.main-content {
flex: 1;
/* 剩余空间 */
padding: 20px;
/* 内容区域的内边距 */
}
.sidebar-container {
display: flex;
flex-direction: column;
width: 200px;
min-height: 100vh;
background-color: #545c64;
}
.collapse-button {
margin: 10px;
align-self: flex-end;
background-color: #545c64;
color: #fff;
border: none;
}
.sidebar-logo { .sidebar-logo {
display: flex; display: flex;
@ -179,40 +158,24 @@ h1 {
background: transparent; /* 关键:透明背景 */ background: transparent; /* 关键:透明背景 */
position: relative; position: relative;
z-index: 1; z-index: 1;
.logo {
width: 50px;
height: 50px;
margin-bottom: 10px;
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5)); /* 添加微光效果 */
}
.system-name {
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-size: 15px;
font-weight: 500;
letter-spacing: 1px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); /* 文字阴影增强可读性 */
}
} }
.logo {
width: 50px;
height: 50px;
margin-bottom: 10px;
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5)); /* 添加微光效果 */
}
.system-name {
color: rgba(255, 255, 255, 0.9);
margin: 0;
font-size: 15px;
font-weight: 500;
letter-spacing: 1px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); /* 文字阴影增强可读性 */
}
.el-aside-demo {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 200px;
z-index: 1000;
overflow-y: auto;
background-color: #4d4f52;
border-right: 1px solid;
}
.el-container-right {
margin-left: 0;
width: calc(100vw - 220px);
}
.el-menu-vertical-demo { .el-menu-vertical-demo {
flex: 1; flex: 1;
@ -244,8 +207,35 @@ h1 {
margin-right: 10px; margin-right: 10px;
} }
.router-link-active { .avatar-container {
color: #ffd04b; height: 50px;
overflow: hidden;
position: relative;
float: right;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
margin-right: 40px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
i {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
} }
</style> </style>