add organize opera

This commit is contained in:
fancl 2024-12-12 17:39:04 +08:00
parent 0d4039cf34
commit b49c431a8e
25 changed files with 834 additions and 650 deletions

91
api.go
View File

@ -3,9 +3,15 @@ package moto
import ( import (
"git.nobla.cn/golang/kos" "git.nobla.cn/golang/kos"
"git.nobla.cn/golang/kos/entry/http" "git.nobla.cn/golang/kos/entry/http"
"git.nobla.cn/golang/moto/internal/user" "git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/internal/user/passport" "git.nobla.cn/golang/moto/internal/organize"
"git.nobla.cn/golang/moto/internal/user/types" "git.nobla.cn/golang/moto/internal/organize/passport"
"git.nobla.cn/golang/moto/internal/organize/types"
"git.nobla.cn/golang/moto/version"
"git.nobla.cn/golang/rest"
restTypes "git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
"strconv"
) )
func (svr *Server) handleLogin(ctx *http.Context) (err error) { func (svr *Server) handleLogin(ctx *http.Context) (err error) {
@ -32,7 +38,7 @@ func (svr *Server) handleProfile(ctx *http.Context) (err error) {
var ( var (
profile *types.Profile profile *types.Profile
) )
if profile, err = user.Profile(ctx.Context(), ctx.User().ID); err != nil { if profile, err = organize.Profile(ctx.Context(), ctx.User().ID); err != nil {
return ctx.Error(http.ErrTemporaryUnavailable, err.Error()) return ctx.Error(http.ErrTemporaryUnavailable, err.Error())
} }
return ctx.Success(profile) return ctx.Success(profile)
@ -42,12 +48,85 @@ func (svr *Server) handleGetConfigure(ctx *http.Context) (err error) {
return ctx.Success(map[string]string{}) return ctx.Success(map[string]string{})
} }
func (svr *Server) handleListSchema(ctx *http.Context) (err error) {
var (
schemas []*restTypes.Schema
)
scenario := ctx.Query("scenario")
if scenario == "" {
schemas, err = rest.GetSchemas(
ctx.Request().Context(),
db.WithContext(ctx.Request().Context()),
ctx.User().Get("domain"),
version.ProductName,
ctx.Param("table"),
)
} else {
schemas, err = rest.VisibleSchemas(
ctx.Request().Context(),
db.WithContext(ctx.Request().Context()),
ctx.User().Get("domain"),
version.ProductName,
ctx.Param("table"),
scenario,
)
}
if err != nil {
return ctx.Error(http.ErrResourceNotFound, err.Error())
} else {
return ctx.Success(schemas)
}
}
func (svr *Server) handleSaveSchema(ctx *http.Context) (err error) {
schemas := make([]*restTypes.Schema, 0)
if err = ctx.Bind(&schemas); err != nil {
return ctx.Error(http.ErrInvalidPayload, err.Error())
}
domainName := ctx.User().Get("domain")
for i, _ := range schemas {
schemas[i].Domain = domainName
}
if err = db.WithContext(ctx.Request().Context()).Transaction(func(tx *gorm.DB) (errTx error) {
for _, scm := range schemas {
if errTx = tx.Save(scm).Error; errTx != nil {
return
}
}
return
}); err == nil {
return ctx.Success(map[string]interface{}{
"count": len(schemas),
"state": "success",
})
} else {
return ctx.Error(http.ErrTemporaryUnavailable, err.Error())
}
}
func (svr *Server) handleDeleteSchema(ctx *http.Context) (err error) {
id, _ := strconv.Atoi(ctx.Param("id"))
model := &restTypes.Schema{Id: uint64(id)}
if err = db.WithContext(ctx.Request().Context()).Delete(model).Error; err == nil {
return ctx.Success(map[string]any{
"id": id,
})
} else {
return ctx.Error(http.ErrResourceDelete, err.Error())
}
}
func (svr *Server) routes() { func (svr *Server) routes() {
kos.Http().Use(user.AuthMiddleware) kos.Http().Use(organize.AuthMiddleware)
user.AllowUri("/passport/login") organize.AllowUri("/passport/login")
kos.Http().Handle(http.MethodPost, "/passport/login", svr.handleLogin) kos.Http().Handle(http.MethodPost, "/passport/login", svr.handleLogin)
kos.Http().Handle(http.MethodDelete, "/passport/logout", svr.handleLogout) kos.Http().Handle(http.MethodDelete, "/passport/logout", svr.handleLogout)
kos.Http().Handle(http.MethodGet, "/user/profile", svr.handleProfile) kos.Http().Handle(http.MethodGet, "/user/profile", svr.handleProfile)
kos.Http().Handle(http.MethodGet, "/user/configures", svr.handleGetConfigure) kos.Http().Handle(http.MethodGet, "/user/configures", svr.handleGetConfigure)
kos.Http().Handle(http.MethodGet, "/rest/schema/:module/:table", svr.handleListSchema)
kos.Http().Handle(http.MethodPut, "/rest/schema/:module/:table", svr.handleSaveSchema)
kos.Http().Handle(http.MethodDelete, "/rest/schema/:id", svr.handleDeleteSchema)
} }

View File

@ -5,8 +5,10 @@ import (
"git.nobla.cn/golang/moto/config" "git.nobla.cn/golang/moto/config"
"git.nobla.cn/golang/rest/types" "git.nobla.cn/golang/rest/types"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
lru "github.com/hashicorp/golang-lru/v2"
mysqlDriver "gorm.io/driver/mysql" mysqlDriver "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"reflect"
"time" "time"
) )
@ -14,6 +16,73 @@ var (
db *gorm.DB db *gorm.DB
) )
var (
cacheInstance, _ = lru.New[string, *cacheEntry](64)
)
func WithDepend(s string, args ...any) CacheOption {
return func(o *CacheOptions) {
o.dependSQL = s
o.dependArgs = args
}
}
func TryCache(ctx context.Context, key string, f CachingFunc, cbs ...CacheOption) (value any, err error) {
var (
ok bool
hasDependValue bool
dependValue any
)
opts := &CacheOptions{}
for _, cb := range cbs {
cb(opts)
}
//从缓存加载数据
if value, ok = cacheInstance.Get(key); ok {
entry := value.(*cacheEntry)
if opts.dependSQL == "" {
return entry.storeValue, nil
}
//如果频繁访问,不检查依赖
if time.Since(entry.lastChecked) < time.Millisecond*500 {
return entry.storeValue, nil
}
//对比依赖值
if err = WithContext(ctx).Raw(opts.dependSQL, opts.dependArgs...).Scan(&dependValue).Error; err == nil {
hasDependValue = true
if reflect.DeepEqual(entry.compareValue, dependValue) {
entry.lastChecked = time.Now()
return entry.storeValue, nil
} else {
cacheInstance.Remove(key)
}
}
}
//从数据库加载数据
if value, err = f(WithContext(ctx)); err == nil {
if !hasDependValue {
if err = WithContext(ctx).Raw(opts.dependSQL, opts.dependArgs...).Scan(&dependValue).Error; err == nil {
cacheInstance.Add(key, &cacheEntry{
compareValue: dependValue,
storeValue: value,
createdAt: time.Now(),
lastChecked: time.Now(),
})
}
} else {
cacheInstance.Add(key, &cacheEntry{
compareValue: dependValue,
storeValue: value,
createdAt: time.Now(),
lastChecked: time.Now(),
})
}
return value, nil
} else {
return nil, err
}
}
func Init(ctx context.Context, cfg config.Database, plugins ...gorm.Plugin) (err error) { func Init(ctx context.Context, cfg config.Database, plugins ...gorm.Plugin) (err error) {
dbCfg := &mysql.Config{ dbCfg := &mysql.Config{
Net: "tcp", Net: "tcp",

24
common/db/types.go 100644
View File

@ -0,0 +1,24 @@
package db
import (
"gorm.io/gorm"
"time"
)
type (
CachingFunc func(tx *gorm.DB) (any, error)
CacheOptions struct {
dependSQL string
dependArgs []any
}
CacheOption func(o *CacheOptions)
cacheEntry struct {
storeValue any
compareValue any
createdAt time.Time
lastChecked time.Time
}
)

12
go.mod
View File

@ -4,9 +4,10 @@ go 1.22.9
require ( require (
git.nobla.cn/golang/kos v0.1.32 git.nobla.cn/golang/kos v0.1.32
git.nobla.cn/golang/rest v0.0.1 git.nobla.cn/golang/rest v0.0.2
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/mssola/useragent v1.0.0 github.com/mssola/useragent v1.0.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
@ -15,14 +16,21 @@ require (
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/peterh/liner v1.2.2 // indirect github.com/peterh/liner v1.2.2 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
) )

24
go.sum
View File

@ -2,16 +2,28 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.nobla.cn/golang/kos v0.1.32 h1:sFVCA7vKc8dPUd0cxzwExOSPX2mmMh2IuwL6cYS1pBc= git.nobla.cn/golang/kos v0.1.32 h1:sFVCA7vKc8dPUd0cxzwExOSPX2mmMh2IuwL6cYS1pBc=
git.nobla.cn/golang/kos v0.1.32/go.mod h1:35Z070+5oB39WcVrh5DDlnVeftL/Ccmscw2MZFe9fUg= git.nobla.cn/golang/kos v0.1.32/go.mod h1:35Z070+5oB39WcVrh5DDlnVeftL/Ccmscw2MZFe9fUg=
git.nobla.cn/golang/rest v0.0.1 h1:atEF73F7NuzYGWzO4+H2qHwgtwV+omG1paEj1DJ5RN8= git.nobla.cn/golang/rest v0.0.2 h1:b5hmGuVb1zXFQ8O2AYCi8628JGftgH2TL5BLftCN1OU=
git.nobla.cn/golang/rest v0.0.1/go.mod h1:tGDOul2GGJtxk6fAeu+YLpMt/Up/TsBonTkygymN/wE= git.nobla.cn/golang/rest v0.0.2/go.mod h1:tGDOul2GGJtxk6fAeu+YLpMt/Up/TsBonTkygymN/wE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -20,6 +32,8 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mssola/useragent v1.0.0 h1:WRlDpXyxHDNfvZaPEut5Biveq86Ze4o4EMffyMxmH5o= github.com/mssola/useragent v1.0.0 h1:WRlDpXyxHDNfvZaPEut5Biveq86Ze4o4EMffyMxmH5o=
@ -32,10 +46,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -0,0 +1,112 @@
package organize
import (
"context"
"fmt"
"git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/models"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
"strings"
)
func RecursiveDepartment(ctx context.Context, parent string, level int, departments []*models.Department) []*types.TypeValue {
var (
child []*types.TypeValue
)
values := make([]*types.TypeValue, 0)
for _, dept := range departments {
if dept.Parent == parent {
values = append(values, &types.TypeValue{
Label: "|-" + strings.Repeat("--", level) + dept.Name,
Value: dept.ID,
})
child = RecursiveDepartment(ctx, dept.ID, level+1, departments)
if len(child) > 0 {
for _, row := range child {
values = append(values, row)
}
}
}
}
return values
}
func RecursiveNestedDepartment(ctx context.Context, parent string, level int, departments []*models.Department) []*types.NestedValue {
values := make([]*types.NestedValue, 0)
for _, dept := range departments {
if dept.Parent == parent {
v := &types.NestedValue{
Label: dept.Name,
Value: dept.ID,
Children: RecursiveNestedDepartment(ctx, dept.ID, level+1, departments),
}
values = append(values, v)
}
}
return values
}
func DepartmentUserNested(ctx context.Context, domainName string) []*types.NestedValue {
var (
err error
value any
users []*models.User
departments []*models.Department
)
if value, err = db.TryCache(ctx, fmt.Sprintf("domain:%s:departments", domainName), func(tx *gorm.DB) (any, error) {
departments = make([]*models.Department, 0)
err = tx.Where("domain=?", domainName).Order("created_at ASC").Find(&departments).Error
return departments, err
}, db.WithDepend("SELECT max(`updated_at`) FROM `departments` WHERE `domain`=?", domainName)); err == nil {
departments = value.([]*models.Department)
} else {
return nil
}
if value, err = db.TryCache(ctx, fmt.Sprintf("domain:%s:users", domainName), func(tx *gorm.DB) (any, error) {
users = make([]*models.User, 0)
err = tx.Where("domain=?", domainName).Order("uid ASC").Find(&users).Error
return users, err
}, db.WithDepend("SELECT max(`updated_at`) FROM `users` WHERE `domain`=?", domainName)); err == nil {
users = value.([]*models.User)
} else {
return nil
}
depts := RecursiveDepartment(ctx, "", 0, departments)
values := make([]*types.NestedValue, 0)
for _, dept := range depts {
v := &types.NestedValue{
Label: dept.Label,
Value: dept.Value,
Children: make([]*types.NestedValue, 0),
}
for _, user := range users {
if user.Department == v.Value {
v.Children = append(v.Children, &types.NestedValue{
Label: fmt.Sprintf("%s(%s)", user.Username, user.UID),
Value: user.UID,
})
}
}
values = append(values, v)
}
return values
}
func DepartmentTypes(ctx context.Context, domainName string) []*types.TypeValue {
result, err := db.TryCache(ctx, fmt.Sprintf("domain:%s:department:types", domainName), func(tx *gorm.DB) (any, error) {
values := make([]*models.Department, 0)
if err := db.WithContext(ctx).Where("domain=?", domainName).Find(&values).Error; err == nil {
return RecursiveDepartment(ctx, "", 0, values), nil
} else {
return nil, err
}
}, db.WithDepend("SELECT max(`updated_at`) FROM `departments` WHERE `domain`=?", domainName))
if err == nil {
return result.([]*types.TypeValue)
}
return nil
}

View File

@ -1,9 +1,9 @@
package user package organize
import ( import (
"git.nobla.cn/golang/kos/entry/http" "git.nobla.cn/golang/kos/entry/http"
"git.nobla.cn/golang/moto/internal/user/passport" "git.nobla.cn/golang/moto/internal/organize/passport"
"git.nobla.cn/golang/moto/internal/user/types" "git.nobla.cn/golang/moto/internal/organize/types"
"os" "os"
"strings" "strings"
) )

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"git.nobla.cn/golang/kos/pkg/cache" "git.nobla.cn/golang/kos/pkg/cache"
"git.nobla.cn/golang/moto/common/db" "git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/internal/user/types" "git.nobla.cn/golang/moto/internal/organize/types"
"git.nobla.cn/golang/moto/models" "git.nobla.cn/golang/moto/models"
"github.com/mssola/useragent" "github.com/mssola/useragent"
"strings" "strings"

View File

@ -1,9 +1,9 @@
package user package organize
import ( import (
"context" "context"
"git.nobla.cn/golang/moto/common/db" "git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/internal/user/types" "git.nobla.cn/golang/moto/internal/organize/types"
) )
func Profile(ctx context.Context, uid string) (profile *types.Profile, err error) { func Profile(ctx context.Context, uid string) (profile *types.Profile, err error) {

View File

@ -0,0 +1,21 @@
package organize
import (
"context"
"fmt"
"git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/models"
"git.nobla.cn/golang/rest"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
)
func RoleTypes(ctx context.Context, domainName string) []*types.TypeValue {
result, err := db.TryCache(ctx, fmt.Sprintf("domain:%s:role:types", domainName), func(tx *gorm.DB) (any, error) {
return rest.ModelTypes(ctx, tx, &models.Role{}, domainName, "name", "id"), nil
}, db.WithDepend("SELECT max(`updated_at`) FROM `roles` WHERE `domain`=?", domainName))
if err == nil {
return result.([]*types.TypeValue)
}
return nil
}

View File

@ -0,0 +1,32 @@
package organize
import (
"context"
"fmt"
"git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/models"
"git.nobla.cn/golang/rest/types"
"gorm.io/gorm"
)
func UserTypes(ctx context.Context, domainName string) []*types.TypeValue {
result, err := db.TryCache(ctx, fmt.Sprintf("domain:%s:user:types", domainName), func(tx *gorm.DB) (any, error) {
values := make([]*models.User, 0)
err := tx.Where("domain=?", domainName).Order("uid ASC").Find(&values).Error
if err != nil {
return nil, err
}
data := make([]*types.TypeValue, 0, len(values))
for _, row := range values {
data = append(data, &types.TypeValue{
Label: fmt.Sprintf("%s(%s)", row.Username, row.UID),
Value: row.UID,
})
}
return data, nil
}, db.WithDepend("SELECT max(`updated_at`) FROM `users` WHERE `domain`=?", domainName))
if err == nil {
return result.([]*types.TypeValue)
}
return nil
}

View File

@ -7,8 +7,9 @@ import (
"git.nobla.cn/golang/moto/common/db" "git.nobla.cn/golang/moto/common/db"
"git.nobla.cn/golang/moto/config" "git.nobla.cn/golang/moto/config"
"git.nobla.cn/golang/moto/models" "git.nobla.cn/golang/moto/models"
"git.nobla.cn/golang/moto/version"
"git.nobla.cn/golang/rest" "git.nobla.cn/golang/rest"
"git.nobla.cn/golang/rest/plugins/identity"
"git.nobla.cn/golang/rest/plugins/validate"
"io" "io"
"net/http" "net/http"
) )
@ -20,7 +21,7 @@ type Server struct {
} }
func (svr *Server) prepare() (err error) { func (svr *Server) prepare() (err error) {
if err = db.Init(svr.ctx, svr.cfg.Database); err != nil { if err = db.Init(svr.ctx, svr.cfg.Database, identity.New(), validate.New()); err != nil {
return return
} }
values := []any{ values := []any{
@ -29,15 +30,15 @@ func (svr *Server) prepare() (err error) {
&models.Login{}, &models.Login{},
&models.Department{}, &models.Department{},
} }
rest.SetHttpRouter(svr)
for _, item := range values { for _, item := range values {
if err = db.DB().AutoMigrate(item); err != nil { if err = db.DB().AutoMigrate(item); err != nil {
return return
} }
if err = rest.AutoMigrate(svr.ctx, db.DB(), item, rest.WithoutDomain(), rest.WithModuleName(version.ProductName)); err != nil { if err = rest.AutoMigrate(svr.ctx, db.DB(), item, rest.WithoutDomain(), rest.WithModuleName("rest")); err != nil {
return return
} }
} }
rest.SetHttpRouter(svr)
return return
} }

View File

@ -17,6 +17,9 @@ const props = defineProps({
}, },
tableName: { tableName: {
type: String, type: String,
},
apiPrefix: {
type: String,
} }
}) })
@ -27,7 +30,7 @@ const loading = ref(false)
const crud = ref(new CRUD({ const crud = ref(new CRUD({
moduleName: props.moduleName, moduleName: props.moduleName,
tableName: props.tableName, tableName: props.tableName,
apiPrefix: '' apiPrefix: props.apiPrefix
})) }))
onMounted(() => { onMounted(() => {

View File

@ -25,8 +25,8 @@
</div> </div>
<div class="segment-body" :class="disableHeader ? 'plain' : ''"> <div class="segment-body" :class="disableHeader ? 'plain' : ''">
<div class="segment-search"> <div class="segment-search">
<active-form :schemas="schemas" :size="size" scenario="search" :model="searchModel" :inline="true" <active-form :schemas="schemas" :size="size" scenario="search" :model="searchModel"
:actions="searchButtons" :auto-commit="autoQuery"> :inline="true" :actions="searchButtons" :auto-commit="autoQuery">
<template #default="{ model, schema }"> <template #default="{ model, schema }">
<slot name="searchform" :model="model" :schema="schema"></slot> <slot name="searchform" :model="model" :schema="schema"></slot>
</template> </template>
@ -73,7 +73,8 @@
</el-drawer> </el-drawer>
</template> </template>
<template v-else> <template v-else>
<el-dialog v-model="formVisible" :title="formTitle" :width="formWidth" draggable :destroy-on-close="true"> <el-dialog v-model="formVisible" :title="formTitle" :width="formWidth" draggable
:destroy-on-close="true">
<active-form :schemas="schemas" :size="size" :scenario="formScenario" :model="formModel" <active-form :schemas="schemas" :size="size" :scenario="formScenario" :model="formModel"
:actions="formButtons"> :actions="formButtons">
<template #default="{ model, schema }"> <template #default="{ model, schema }">
@ -138,6 +139,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
apiPrefix: {
type: String,
default: ''
},
disableToolbar: { // disableToolbar: { //
type: Boolean, type: Boolean,
default: false, default: false,

View File

@ -7,13 +7,14 @@ class CRUD {
opts = Object.assign({ opts = Object.assign({
moduleName: '', moduleName: '',
tableName: '', tableName: '',
apiPrefix: '/v1', apiPrefix: '',
schemas: [], schemas: [],
}, opts); }, opts);
this.opts = opts this.opts = opts
this.primaryKey = '' this.primaryKey = ''
this.modelName = opts.moduleName this.modelName = opts.moduleName
this.tableName = opts.tableName this.tableName = opts.tableName
this.apiPrefix = opts.apiPrefix || opts.moduleName || 'rest'
this.models = [] this.models = []
this.sortable = [] this.sortable = []
this.queryParams = {} this.queryParams = {}
@ -140,30 +141,31 @@ class CRUD {
__buildModelUri(moduleName, tableNanme, scenario, primaryKey) { __buildModelUri(moduleName, tableNanme, scenario, primaryKey) {
primaryKey = primaryKey || '' primaryKey = primaryKey || ''
let uri = this.opts.apiPrefix ? this.opts.apiPrefix : ''; let uri = '';
let apiPrefix = this.apiPrefix;
let pluralizeName = pluralize.plural(tableNanme); let pluralizeName = pluralize.plural(tableNanme);
let singularName = pluralize.singular(tableNanme); let singularName = pluralize.singular(tableNanme);
switch (scenario) { switch (scenario) {
case 'create': case 'create':
uri += `/${moduleName}/${singularName}` uri += `/${apiPrefix}/${singularName}`
break break
case 'update': case 'update':
uri += `/${moduleName}/${singularName}/${primaryKey}` uri += `/${apiPrefix}/${singularName}/${primaryKey}`
break break
case 'delete': case 'delete':
uri += `/${moduleName}/${singularName}/${primaryKey}` uri += `/${apiPrefix}/${singularName}/${primaryKey}`
break break
case 'get': case 'get':
uri += `/${moduleName}/${singularName}/${primaryKey}` uri += `/${apiPrefix}/${singularName}/${primaryKey}`
break break
case 'search': case 'search':
uri += `/${moduleName}/${pluralizeName}` uri += `/${apiPrefix}/${pluralizeName}`
break break
case 'export': case 'export':
uri += `/${moduleName}/${singularName}-export` uri += `/${apiPrefix}/${singularName}-export`
break break
case 'import': case 'import':
uri += `/${moduleName}/${singularName}-import` uri += `/${apiPrefix}/${singularName}-import`
break break
} }
return uri return uri

View File

@ -87,622 +87,6 @@ const menu = [
}, },
] ]
}, },
{
label: "客户管理",
icon: "group",
hidden: false,
route: "/customer",
children: [
{
label: "联系人",
navigation: true,
route: "/customer/contacts",
children: [
{
label: "新建联系人",
hidden: true,
route: "/customer/contact/create",
view: '../views/customer/contact/Form.vue'
},
{
label: "编辑联系人",
hidden: true,
route: "/customer/contact/update",
view: '../views/customer/contact/Form.vue'
},
{
label: "联系人详情",
hidden: true,
route: "/customer/contact/view",
view: '../views/customer/contact/View.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
},
{
label: '详情',
value: 'view'
}
]
}
]
},
{
label: "呼叫任务",
icon: "outbound",
hidden: false,
route: "/outbounds",
children: [
{
label: "全部任务",
hidden: false,
navigation: true,
route: "/outbound/campaigns",
children: [
{
label: "新建任务",
hidden: true,
route: "/outbound/campaign/create",
view: '../views/outbound/campaign/Form.vue'
},
{
label: "编辑任务",
hidden: true,
route: "/outbound/campaign/update",
view: '../views/outbound/campaign/Form.vue'
},
{
label: "任务详情",
hidden: true,
route: "/outbound/campaign/view",
view: '../views/outbound/campaign/View.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
},
{
label: '详情',
value: 'view'
},
{
label: '启动',
value: 'start'
},
{
label: '暂停',
value: 'pause'
},
{
label: '停止',
value: 'stop'
},
{
label: '导入',
value: 'import'
}
]
}
]
},
{
label: "记录报表",
icon: "data",
route: "/record",
children: [
{
label: "话单记录",
route: "/record/cdrs",
children: [
{
label: "通话记录",
route: "/record/cdr/logs"
},
{
label: "来电未接",
route: "/record/cdr/noanswers"
},
{
label: "座席未接",
route: "/record/cdr/usernoanswers"
},
]
},
{
label: "话单报表",
route: "/record/reports",
children: [
{
label: "座席报表",
route: "/record/report/users"
},
{
label: "呼入报表",
route: "/record/report/inbounds"
},
]
},
{
label: "日志记录",
route: "/record/log",
children: [
{
label: "登录记录",
route: "/record/log/logins"
},
{
label: "考勤记录",
route: "/record/log/attendances"
},
{
label: "操作记录",
route: "/record/log/activities"
},
{
label: "导入导出",
route: "/record/log/genfiles"
},
]
},
]
},
{
label: "状态监控",
icon: "jiankong",
route: "/monitor",
children: [
{
label: "座席监控",
route: "/monitor/users",
},
{
label: "话务监控",
route: "/monitor/calls",
},
{
label: "系统监控",
hidden: true,
route: "/monitor/systems",
},
]
},
{
label: "系统配置",
icon: "set",
hidden: false,
route: "/settings",
children: [
{
label: "匹配规则",
route: "/setting/rules",
children: [
{
label: "号码匹配",
navigation: true,
route: "/setting/rule/numbersets",
children: [
{
label: "号码列表",
hidden: true,
route: "/setting/rule/numberset/list"
}
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "正则匹配",
route: "/setting/rule/regexps",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "时间匹配",
route: "/setting/rule/timeslots",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "地区匹配",
route: "/setting/rule/zones",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
}
]
},
{
label: "应用配置",
route: "/setting/destinations",
children: [
{
label: "语言导航",
navigation: true,
route: "/setting/destination/ivrs",
children: [
{
label: "新建技能组",
hidden: true,
route: "/setting/destination/ivr/create",
view: '../views/setting/destination/ivr/Form.vue'
},
{
label: "编辑技能组",
hidden: true,
route: "/setting/destination/ivr/update",
view: '../views/setting/destination/ivr/Form.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "呼叫队列",
navigation: true,
route: "/setting/destination/queues",
children: [
{
label: "新建队列",
hidden: true,
route: "/setting/destination/queue/create",
view: '../views/setting/destination/queue/Form.vue'
},
{
label: "编辑队列",
hidden: true,
route: "/setting/destination/queue/update",
view: '../views/setting/destination/queue/Form.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "语言文件",
route: "/setting/destination/sounds",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "脚本程序",
route: "/setting/destination/scripts",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "快捷功能",
route: "/setting/destination/shortcuts",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "网关管理",
navigation: true,
route: "/setting/destination/gateways",
children: [
{
label: "新建网关",
hidden: true,
route: "/setting/destination/gateway/create",
view: '../views/setting/destination/gateway/Form.vue'
},
{
label: "编辑网关",
hidden: true,
route: "/setting/destination/gateway/update",
view: '../views/setting/destination/gateway/Form.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "设备列表",
route: "/setting/destination/devices",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
]
},
{
label: "路由配置",
route: "/setting/routes",
children: [
{
label: "呼入路由",
route: "/setting/route/inbounds",
navigation: true,
children: [
{
label: "新增路由",
hidden: true,
route: "/setting/route/inbound/create",
view: '../views/setting/route/inbound/Form.vue'
},
{
label: "更新路由",
hidden: true,
route: "/setting/route/inbound/update",
view: '../views/setting/route/inbound/Form.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "呼出路由",
route: "/setting/route/outbounds",
navigation: true,
children: [
{
label: "新增路由",
hidden: true,
route: "/setting/route/outbound/create",
view: '../views/setting/route/outbound/Form.vue'
},
{
label: "更新路由",
hidden: true,
route: "/setting/route/outbound/update",
view: '../views/setting/route/outbound/Form.vue'
},
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
]
},
{
label: "高级设置",
route: "/setting/advanced",
children: [
{
label: "访问控制",
navigation: true,
route: "/setting/advanced/acls",
children: [
{
label: "规则列表",
hidden: true,
route: "/setting/advanced/acl/list"
}
],
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "变量设置",
route: "/setting/advanced/variables",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "状态设置",
route: "/setting/advanced/statuses",
permissions: [
{
label: '新建',
value: 'create'
},
{
label: '更新',
value: 'update'
},
{
label: '删除',
value: 'delete'
}
]
},
{
label: "字段配置",
hidden: true,
route: "/setting/schemas",
},
]
},
]
},
]; ];
export default menu; export default menu;

View File

@ -14,7 +14,7 @@ const props = defineProps({
}) })
const moduleName = computed(() => { const moduleName = computed(() => {
return 'moto' return ''
}) })
const tableName = computed(() => { const tableName = computed(() => {

View File

@ -0,0 +1,144 @@
<template>
<viewer :title="title" :module-name="moduleName" :table-name="tableName" :disable-toolbar="false"
defaultSortable="id" formMode="drawer" :disablePermission="true">
<template #crudform="{ schema, model }">
<el-tree ref="treeElement" :data="permissions" v-if="schema.column == 'permissions'" node-key="id"
v-model="model.permissions" :props="treeOptions" @check-change="handleTreeChange(model)"
:default-checked-keys="getCheckedKeys(model)" show-checkbox></el-tree>
</template>
</viewer>
</template>
<style lang="scss">
.el-tree {
width: 100%;
.el-tree-node {
&.permission-node {
display: inline-block;
&:not(:first-child) {
.el-tree-node__content {
padding-left: 0 !important;
}
}
.el-tree-node__label {
font-size: .78rem;
color: var(--text-color-muted);
}
}
}
}
</style>
<script setup>
import Viewer from '@/components/fragment/Viewer.vue';
import { ref, computed } from 'vue';
import { getMenus, nameize } from '@/assets/js/menu';
const props = defineProps({
title: {
type: String,
}
})
const moduleName = computed(() => {
return ''
})
const tableName = computed(() => {
return 'roles'
})
const treeElement = ref()
const treeOptions = computed(() => {
return {
class: (data, node) => {
if (data.isPermission) {
return 'permission-node'
}
return ''
}
}
})
const gridviewActions = computed(() => {
return [
'edit',
'delete',
];
})
const permissions = computed(() => {
let permissions = buildMenuTree(getMenus())
return [
{
id: 'all',
label: '全部',
isPermission: false,
children: permissions,
}
];
})
const getCheckedKeys = (model) => {
if (!model.permissions) {
return []
} else {
return JSON.parse(model.permissions)
}
}
const handleTreeChange = (model) => {
let permissions = treeElement.value.getCheckedKeys() || [];
model.permissions = JSON.stringify(permissions);
}
const buildPermissionTree = (key, data) => {
let permissions = []
for (let i in data) {
let row = data[i];
let item = {
id: key + "." + row.value,
label: row.label,
isPermission: true,
}
permissions.push(item)
}
return permissions;
}
const buildMenuTree = (data) => {
let values = []
for (let i in data) {
let row = Object.assign({}, data[i]);
let item = {
id: nameize(row),
label: row.label,
isPermission: false,
children: []
}
if (row.hidden && !Array.isArray(row.permissions)) {
continue
}
if (Array.isArray(row.children)) {
item.children = buildMenuTree(row.children)
}
if (Array.isArray(row.permissions)) {
let childrens = buildPermissionTree(item.id, row.permissions)
for (let child of childrens) {
item.children.push(child)
}
}
values.push(item)
}
return values
}
</script>

View File

@ -0,0 +1,24 @@
<template>
<viewer :title="title" :module-name="moduleName" :table-name="tableName" :disable-toolbar="false"
defaultSortable="uid"></viewer>
</template>
<script setup>
import Viewer from '@/components/fragment/Viewer.vue';
import { computed } from 'vue';
const props = defineProps({
title: {
type: String,
}
})
const moduleName = computed(() => {
return ''
})
const tableName = computed(() => {
return 'users'
})
</script>

View File

@ -0,0 +1,61 @@
<template>
<viewer :title="title" :module-name="moduleName" :table-name="tableName" :disable-toolbar="false"
:disable-permission="true" defaultSortable="-created_at" :default-query="defaultQuery" :readonly="true"
:toolbar-actions="toolbarActions">
<template #searchform="{ model, schema }">
<user-panel v-if="schema.column == 'receiver'" v-model="model['receiver']"></user-panel>
<user-panel v-if="schema.column == 'sender'" v-model="model['sender']"></user-panel>
</template>
</viewer>
</template>
<script setup>
import Viewer from '@/components/fragment/Viewer.vue';
import { computed } from 'vue';
import { readUserMsg } from '@/apis/organize'
import { ElMessage } from 'element-plus';
import { getModelValue } from '@/components/fragment/libs/model';
import UserPanel from '@/components/widgets/UserPanel.vue';
import dayjs from 'dayjs';
const props = defineProps({
title: {
type: String,
}
})
const moduleName = computed(() => {
return 'organize'
})
const tableName = computed(() => {
return 'user_notices'
})
const defaultQuery = computed(() => {
return {
created_at: [dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'), dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')].join('/')
}
})
const toolbarActions = computed(() => {
return ['export', 'batchDelete', {
name: 'read',
label: '设置已读',
selection: true,
callback: (values) => {
let ps = [];
for (let i in values) {
let id = getModelValue(values[i], 'id');
ps.push(readUserMsg(id))
}
Promise.all(ps).then(res => {
ElMessage.success('操作成功');
}).catch(e => {
ElMessage.error(e.message);
})
}
}]
})
</script>

View File

@ -0,0 +1,195 @@
<template>
<div class="profile-container">
<el-row :gutter="20">
<el-col :span="6">
<div class="profile">
<div class="profile-avatar text-center d-flex">
<div class="flex-shrink">
<el-avatar :size="72" :src="avatar" />
</div>
<div class="flex-fill">
<h3>{{ username }}</h3>
<span class="text-muted">{{ description }}</span>
</div>
</div>
<div class="profile-fields">
<preview label="工号" :value="uid"></preview>
<preview label="名称" :value="username"></preview>
<preview label="邮箱" :value="email"></preview>
</div>
</div>
</el-col>
<el-col :span="18">
<div class="profile-tabs">
<el-tabs v-model="activeTab">
<el-tab-pane label="基本资料" name="basic">
<el-form ref="basicForm" :model="basicModel" status-icon label-width="120px" class="segment-form">
<el-form-item label="用户名" prop="oldPassword">
<el-input v-model="basicModel.username" autocomplete="off" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="newPassword">
<el-input v-model="basicModel.email" autocomplete="off" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="个人简介" prop="confirmPassword">
<el-input v-model="basicModel.description" type="textarea" autocomplete="off" rows="10"
placeholder="请介绍下自己" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleUpdateProfile"></el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="密码修改" name="password">
<el-form ref="passwordForm" :model="passwordModel" :rules="passwordRules" status-icon
label-width="120px" class="segment-form">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="passwordModel.oldPassword" type="password" autocomplete="off"
placeholder="请输入旧密码" />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="passwordModel.newPassword" type="password" autocomplete="off"
placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="passwordModel.confirmPassword" type="password" autocomplete="off"
placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleResetPassword"></el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
</div>
</template>
<style lang="scss">
.profile {
box-sizing: border-box;
padding: 1rem;
background-color: white;
border-radius: 6px;
box-shadow: var(--el-box-shadow-light);
}
.profile-tabs{
background-color: white;
border-radius: 6px;
box-shadow: var(--el-box-shadow-light);
}
.profile-container {
box-sizing: border-box;
padding: 1rem;
.profile-avatar {
margin-bottom: 2rem;
.el-avatar {
box-shadow: var(--el-box-shadow);
margin-right: 1.2rem;
}
.el-avatar {
margin-bottom: 1.8rem;
}
h3 {
margin: .5rem 0 10px 0;
}
}
.profile-desc {
h4 {
margin: .5rem 0;
}
p {
text-indent: 2rem;
font-size: .78rem;
line-height: 1.48rem;
}
margin-bottom: 2.8rem;
}
}
</style>
<script setup>
import { ref } from 'vue'
import useUserStore from '@/stores/user'
import Preview from '@/components/widgets/Preview.vue';
import { storeToRefs } from 'pinia';
import { updateProfile, resetPassword } from '@/apis/organize'
import { ElMessage } from 'element-plus';
const userStore = useUserStore()
const { avatar, username, email, uid, description } = storeToRefs(userStore)
const activeTab = ref('basic')
const basicModel = ref({
username: username,
email: email,
description: description,
})
const passwordForm = ref()
const passwordModel = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const validateConfirmPassword = (rule, value, callback) => {
if (value != passwordModel.value.newPassword) {
callback(new Error("两次密码输入不一致"))
} else if (!/(?=.*[0-9])(?=.*[a-zA-Z]).{6,20}/.test(value)) {
callback(new Error("密码必须包含数字和字母, 长度6-20位"))
} else {
callback()
}
}
const passwordRules = ref({
oldPassword: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
confirmPassword: [
{ validator: validateConfirmPassword, trigger: 'blur' }
]
})
const handleUpdateProfile = () => {
updateProfile(basicModel.value).then(res => {
}).catch(e => {
ElMessage.error(e.message)
})
}
const handleResetPassword = () => {
passwordForm.value.validate().then(valid => {
if (valid) {
resetPassword(passwordModel.value.oldPassword, passwordModel.value.newPassword).then(res => {
ElMessage.success('密码重置成功')
}).catch(e => {
ElMessage.error(e.message)
})
}
}).catch(e => {
})
}
</script>