提交基础框架
This commit is contained in:
parent
ed42db709f
commit
0d4039cf34
50
api.go
50
api.go
|
@ -1,5 +1,53 @@
|
|||
package moto
|
||||
|
||||
func (svr *Server) routes() {
|
||||
import (
|
||||
"git.nobla.cn/golang/kos"
|
||||
"git.nobla.cn/golang/kos/entry/http"
|
||||
"git.nobla.cn/golang/moto/internal/user"
|
||||
"git.nobla.cn/golang/moto/internal/user/passport"
|
||||
"git.nobla.cn/golang/moto/internal/user/types"
|
||||
)
|
||||
|
||||
func (svr *Server) handleLogin(ctx *http.Context) (err error) {
|
||||
var (
|
||||
tk *types.Tokenize
|
||||
req *passport.LoginRequest
|
||||
)
|
||||
req = &passport.LoginRequest{}
|
||||
if err = ctx.Bind(req); err != nil {
|
||||
return ctx.Error(http.ErrInvalidPayload, err.Error())
|
||||
}
|
||||
if tk, err = passport.Login(ctx.Context(), req); err != nil {
|
||||
return ctx.Error(http.ErrPermissionDenied, err.Error())
|
||||
}
|
||||
return ctx.Success(tk)
|
||||
}
|
||||
|
||||
func (svr *Server) handleLogout(ctx *http.Context) (err error) {
|
||||
passport.Logout(ctx.Context(), ctx.User().Get("token"))
|
||||
return ctx.Success("logout")
|
||||
}
|
||||
|
||||
func (svr *Server) handleProfile(ctx *http.Context) (err error) {
|
||||
var (
|
||||
profile *types.Profile
|
||||
)
|
||||
if profile, err = user.Profile(ctx.Context(), ctx.User().ID); err != nil {
|
||||
return ctx.Error(http.ErrTemporaryUnavailable, err.Error())
|
||||
}
|
||||
return ctx.Success(profile)
|
||||
}
|
||||
|
||||
func (svr *Server) handleGetConfigure(ctx *http.Context) (err error) {
|
||||
return ctx.Success(map[string]string{})
|
||||
}
|
||||
|
||||
func (svr *Server) routes() {
|
||||
kos.Http().Use(user.AuthMiddleware)
|
||||
user.AllowUri("/passport/login")
|
||||
|
||||
kos.Http().Handle(http.MethodPost, "/passport/login", svr.handleLogin)
|
||||
kos.Http().Handle(http.MethodDelete, "/passport/logout", svr.handleLogout)
|
||||
kos.Http().Handle(http.MethodGet, "/user/profile", svr.handleProfile)
|
||||
kos.Http().Handle(http.MethodGet, "/user/configures", svr.handleGetConfigure)
|
||||
}
|
||||
|
|
11
go.mod
11
go.mod
|
@ -6,6 +6,8 @@ require (
|
|||
git.nobla.cn/golang/kos v0.1.32
|
||||
git.nobla.cn/golang/rest v0.0.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mssola/useragent v1.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
|
@ -13,21 +15,14 @@ require (
|
|||
|
||||
require (
|
||||
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/now v1.1.5 // 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/patrickmn/go-cache v2.1.0+incompatible // 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
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
|
22
go.sum
22
go.sum
|
@ -7,19 +7,11 @@ git.nobla.cn/golang/rest v0.0.1/go.mod h1:tGDOul2GGJtxk6fAeu+YLpMt/Up/TsBonTkygy
|
|||
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/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.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
|
@ -28,10 +20,10 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|||
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/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/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mssola/useragent v1.0.0 h1:WRlDpXyxHDNfvZaPEut5Biveq86Ze4o4EMffyMxmH5o=
|
||||
github.com/mssola/useragent v1.0.0/go.mod h1:hz9Cqz4RXusgg1EdI4Al0INR62kP7aPSRNHnpU+b85Y=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
|
||||
|
@ -40,16 +32,10 @@ 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/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/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/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"git.nobla.cn/golang/kos/entry/http"
|
||||
"git.nobla.cn/golang/moto/internal/user/passport"
|
||||
"git.nobla.cn/golang/moto/internal/user/types"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
allowUris []string
|
||||
|
||||
CookieName = "MOTO_US"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allowUris = make([]string, 0, 10)
|
||||
}
|
||||
|
||||
func AllowUri(s string) {
|
||||
allowUris = append(allowUris, s)
|
||||
}
|
||||
|
||||
func isAllowed(uriPath string) bool {
|
||||
for _, s := range allowUris {
|
||||
sl := len(s)
|
||||
if sl <= 0 {
|
||||
continue
|
||||
}
|
||||
if s[sl-1] == '*' {
|
||||
if strings.HasPrefix(uriPath, s[:sl-1]) {
|
||||
return true
|
||||
}
|
||||
} else if s == uriPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AuthMiddleware(next http.HandleFunc) http.HandleFunc {
|
||||
return func(ctx *http.Context) (err error) {
|
||||
if isAllowed(ctx.Request().URL.Path) {
|
||||
return next(ctx)
|
||||
}
|
||||
var (
|
||||
pos int
|
||||
accessToken string
|
||||
ui *http.Userinfo
|
||||
tk *types.Tokenize
|
||||
)
|
||||
if accessToken = ctx.Query("access_token"); accessToken != "" {
|
||||
goto __end
|
||||
}
|
||||
if accessToken = ctx.GetCookieValue(CookieName); accessToken != "" {
|
||||
goto __end
|
||||
}
|
||||
if accessToken = ctx.Request().Header.Get("Authorization"); accessToken != "" {
|
||||
goto __end
|
||||
}
|
||||
__end:
|
||||
accessToken = strings.TrimSpace(accessToken)
|
||||
if pos = strings.IndexByte(accessToken, ' '); pos > -1 {
|
||||
accessToken = accessToken[pos+1:]
|
||||
}
|
||||
if tk, err = passport.Validate(ctx.Context(), accessToken); err != nil {
|
||||
err = ctx.Error(http.ErrAccessDenied, "access denied")
|
||||
err = os.ErrPermission
|
||||
return
|
||||
}
|
||||
ui = &http.Userinfo{
|
||||
ID: tk.UID,
|
||||
Name: tk.Name,
|
||||
}
|
||||
ui.Set("token", tk.Token)
|
||||
ctx.SetUser(ui)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package passport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.nobla.cn/golang/kos/pkg/cache"
|
||||
"git.nobla.cn/golang/moto/common/db"
|
||||
"git.nobla.cn/golang/moto/internal/user/types"
|
||||
"git.nobla.cn/golang/moto/models"
|
||||
"github.com/mssola/useragent"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenizeLocker sync.RWMutex
|
||||
tokenizes map[string]*types.Tokenize
|
||||
|
||||
LoginMaxTires = 5
|
||||
)
|
||||
|
||||
func init() {
|
||||
tokenizes = make(map[string]*types.Tokenize)
|
||||
}
|
||||
|
||||
func Login(ctx context.Context, req *LoginRequest) (tk *types.Tokenize, err error) {
|
||||
var (
|
||||
tires int
|
||||
duration time.Duration
|
||||
)
|
||||
tx := db.WithContext(ctx)
|
||||
userModel := &models.User{}
|
||||
if req.Remember {
|
||||
duration = time.Hour * 24
|
||||
} else {
|
||||
duration = time.Hour * 2
|
||||
}
|
||||
cacheKey := fmt.Sprintf("passport:login:%s:attempts", req.RealIP)
|
||||
if LoginMaxTires > 0 {
|
||||
if err = cache.Load(ctx, cacheKey, &tires); err == nil {
|
||||
if tires > LoginMaxTires {
|
||||
err = ErrTooManyAttempts
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = tx.Where("uid=?", req.Username).First(userModel).Error; err == nil {
|
||||
if userModel.Password == req.Password || md5Hash(req.Password) == userModel.Password {
|
||||
tk = types.NewTokenize(userModel.UID, duration)
|
||||
tokenizeLocker.Lock()
|
||||
tokenizes[tk.Token] = tk
|
||||
tokenizeLocker.Unlock()
|
||||
|
||||
tk.Name = userModel.Username
|
||||
ua := useragent.New(req.UserAgent)
|
||||
loginModel := &models.Login{
|
||||
UID: userModel.UID,
|
||||
IP: req.RealIP,
|
||||
Os: ua.OS(),
|
||||
Platform: ua.Platform(),
|
||||
AccessToken: tk.Token,
|
||||
UserAgent: req.UserAgent,
|
||||
}
|
||||
browser, browserVersion := ua.Browser()
|
||||
loginModel.Browser = browser + "/" + browserVersion
|
||||
tx.Save(loginModel)
|
||||
} else {
|
||||
err = ErrInvalidPassword
|
||||
_ = cache.StoreEx(ctx, cacheKey, tires+1, time.Minute*10)
|
||||
}
|
||||
} else {
|
||||
_ = cache.StoreEx(ctx, cacheKey, tires+1, time.Minute*10)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Validate(ctx context.Context, token string) (tk *types.Tokenize, err error) {
|
||||
var (
|
||||
ok bool
|
||||
)
|
||||
tokenizeLocker.Lock()
|
||||
defer tokenizeLocker.Unlock()
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
if tk, ok = tokenizes[token]; !ok {
|
||||
return nil, ErrPermissionDenied
|
||||
} else {
|
||||
if tk.Validate() {
|
||||
return tk, nil
|
||||
} else {
|
||||
delete(tokenizes, token)
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Logout(ctx context.Context, token string) (ok bool) {
|
||||
tokenizeLocker.Lock()
|
||||
if _, ok = tokenizes[token]; ok {
|
||||
delete(tokenizes, token)
|
||||
}
|
||||
tokenizeLocker.Unlock()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package passport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
ErrTooManyAttempts = errors.New("too many attempts")
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Remember bool `json:"remember"`
|
||||
RealIP string `json:"real_ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package passport
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"git.nobla.cn/golang/kos/util/bs"
|
||||
)
|
||||
|
||||
func md5Hash(s string) string {
|
||||
hash := md5.New()
|
||||
hash.Write(bs.StringToBytes(s))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.nobla.cn/golang/moto/common/db"
|
||||
"git.nobla.cn/golang/moto/internal/user/types"
|
||||
)
|
||||
|
||||
func Profile(ctx context.Context, uid string) (profile *types.Profile, err error) {
|
||||
profile = &types.Profile{}
|
||||
tx := db.WithContext(ctx)
|
||||
err = tx.Table("users AS u").
|
||||
Select("u.uid as id", "u.username", "u.avatar", "u.email", "u.description", "u.role", "r.permissions").
|
||||
Joins("left join roles AS r on r.id = u.role").Where("u.uid=? ", uid).
|
||||
First(profile).Error
|
||||
return
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package types
|
||||
|
||||
type (
|
||||
Profile struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Description string `json:"description"`
|
||||
Permissions string `json:"permissions"`
|
||||
}
|
||||
)
|
|
@ -0,0 +1,29 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"git.nobla.cn/golang/kos/util/random"
|
||||
"github.com/google/uuid"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Tokenize struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
ExpireIn int `json:"expire_in"`
|
||||
ExpiredAt time.Time `json:"-"`
|
||||
}
|
||||
|
||||
func (tk *Tokenize) Validate() bool {
|
||||
return time.Now().Before(tk.ExpiredAt)
|
||||
}
|
||||
|
||||
func NewTokenize(uid string, duration time.Duration) *Tokenize {
|
||||
return &Tokenize{
|
||||
UID: uid,
|
||||
Token: random.String(32) + strings.ReplaceAll(uuid.New().String(), "-", ""),
|
||||
ExpireIn: int(duration / time.Second),
|
||||
ExpiredAt: time.Now().Add(duration),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package models
|
||||
|
||||
type Login struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:20" comment:"ID" scenarios:"view;export"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime" comment:"登录时间" scenarios:"list;search;view;export" position:"2"`
|
||||
Domain string `json:"domain" gorm:"index;size:60;not null;default:'default'" comment:"域" scenarios:"export"`
|
||||
UID string `json:"uid" gorm:"column:uid;index;size:20;not null;default:''" props:"match:exactly" comment:"座席" format:"user" scenarios:"search;list;export" position:"1"`
|
||||
IP string `json:"ip" gorm:"size:20;not null;default:''" comment:"IP" scenarios:"search;list;export"`
|
||||
Browser string `json:"browser" gorm:"size:200;not null;default:''" comment:"浏览器" scenarios:"search;list;export"`
|
||||
Os string `json:"os" gorm:"size:200;not null;default:''" comment:"操作系统" scenarios:"search;list;export"`
|
||||
Platform string `json:"platform" gorm:"size:200;not null;default:''" comment:"平台" scenarios:"search;list;export"`
|
||||
AccessToken string `json:"access_token" gorm:"size:200;not null;default:''" comment:"访问令牌" scenarios:"export"`
|
||||
UserAgent string `json:"user_agent" gorm:"size:1024;not null;default:''" comment:"UserAgent" scenarios:"export"`
|
||||
}
|
||||
|
||||
func (model *Login) TableName() string {
|
||||
return "user_logins"
|
||||
}
|
|
@ -26,6 +26,7 @@ func (svr *Server) prepare() (err error) {
|
|||
values := []any{
|
||||
&models.User{},
|
||||
&models.Role{},
|
||||
&models.Login{},
|
||||
&models.Department{},
|
||||
}
|
||||
for _, item := range values {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
|
@ -0,0 +1,29 @@
|
|||
# web
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MOTO</title>
|
||||
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_3832926_ya7vh14nje.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.7.9",
|
||||
"color": "^4.2.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.9.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.2.6",
|
||||
"pinia-plugin-persistedstate": "^4.1.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"query-string": "^9.1.1",
|
||||
"sass": "^1.82.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.4.5",
|
||||
"vue3-puzzle-vcode": "^1.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-vue-devtools": "^7.6.5"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<RouterView />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick, provide } from 'vue';
|
||||
import { RouterView, useRouter } from 'vue-router'
|
||||
import zhCN from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import enUS from 'element-plus/dist/locale/en.mjs'
|
||||
import useSystemStore from '@/stores/system'
|
||||
import useUserStore from '@/stores/user'
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const locale = computed(() => {
|
||||
const { lang } = storeToRefs(systemStore)
|
||||
return lang.value == 'zh-CN' ? zhCN : enUS
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const { lang } = storeToRefs(systemStore)
|
||||
if (lang.value === '') {
|
||||
systemStore.setLanguage(navigator.language || navigator.userLanguage)
|
||||
}
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta && to.meta.allowGuest) {
|
||||
next();
|
||||
} else {
|
||||
if (userStore.isAuthorization()) {
|
||||
systemStore.closeFlowSidebar()
|
||||
next()
|
||||
} else {
|
||||
next('/login');
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,117 @@
|
|||
import request, { getBaseUrl } from './request'
|
||||
|
||||
export function getUserProfile() {
|
||||
return request.get(`/user/profile`)
|
||||
}
|
||||
|
||||
export function getUserInfo(uid) {
|
||||
return request.get(`/user/info/${uid}`)
|
||||
}
|
||||
|
||||
export function userLogin(username, password) {
|
||||
let domain = 'test.cc.echo.me';
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
domain = window.location.hostname;
|
||||
}
|
||||
return request.post(`/passport/login`, {
|
||||
"domain": domain,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"remember": true
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserTypes() {
|
||||
return request.get('/organize/user-types')
|
||||
}
|
||||
|
||||
export function userLogout() {
|
||||
return request.delete('/passport/logout')
|
||||
}
|
||||
|
||||
export function updateProfile(data) {
|
||||
return request.put('/organize/profile', data)
|
||||
}
|
||||
|
||||
export function resetPassword(oldPwd, newPwd) {
|
||||
return request.put('/organize/password', {
|
||||
old_password: oldPwd,
|
||||
new_password: newPwd
|
||||
})
|
||||
}
|
||||
|
||||
export function lastLogin() {
|
||||
return request.get('/organize/last-login');
|
||||
}
|
||||
|
||||
export function getNestedDepartments() {
|
||||
return request.get('/organize/department-nested')
|
||||
}
|
||||
|
||||
export function getDepartmentUsers(id) {
|
||||
return request.get(`/organize/department-users/${id}`)
|
||||
}
|
||||
|
||||
export function getUsers(id) {
|
||||
return request.get(`/organize/user-list`)
|
||||
}
|
||||
|
||||
export function getClientHello() {
|
||||
return request.post(`/organize/client-hello`)
|
||||
}
|
||||
|
||||
export function getUserAvatar(uid) {
|
||||
return `${getBaseUrl()}/organize/avatar/${uid}`
|
||||
}
|
||||
|
||||
export function setUserAttr(name, value) {
|
||||
let data = '';
|
||||
if (typeof value !== 'string') {
|
||||
try {
|
||||
data = JSON.stringify(value)
|
||||
} catch (e) {
|
||||
data = value
|
||||
}
|
||||
} else {
|
||||
data = value
|
||||
}
|
||||
return request.put(`/organize/user-variable`, {
|
||||
name: name,
|
||||
value: data,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserAttr(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.get(`/organize/user-variable/${name}`).then(res => {
|
||||
if (res.value) {
|
||||
try {
|
||||
let data = JSON.parse(res.value);
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
resolve(res.value)
|
||||
}
|
||||
} else {
|
||||
reject(new Error("not found"))
|
||||
}
|
||||
}).catch(e => {
|
||||
reject(e);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function getUnreadMsgCount() {
|
||||
return request.get(`/organize/unread/count`)
|
||||
}
|
||||
|
||||
export function readUserMsg(id) {
|
||||
return request.put(`/organize/notice-read/${id}`)
|
||||
}
|
||||
|
||||
export function getDepartmentUserNested() {
|
||||
return request.put(`/organize/department-users`)
|
||||
}
|
||||
|
||||
export function getStatusTypes() {
|
||||
return request.post(`/organize/status-types`)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: getBaseUrl()
|
||||
});
|
||||
|
||||
export function getBaseHost() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return location.host;
|
||||
} else {
|
||||
return 'api-dev.echo.me'
|
||||
}
|
||||
}
|
||||
|
||||
export function getBaseUrl() {
|
||||
let host = getBaseHost();
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return location.protocol + '//' + host;
|
||||
} else {
|
||||
return location.protocol + '//' + host
|
||||
}
|
||||
}
|
||||
|
||||
class HttpError extends Error {
|
||||
constructor(message, code) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.name = 'HttpError'
|
||||
}
|
||||
}
|
||||
|
||||
client.interceptors.request.use(req => {
|
||||
let accessToken = sessionStorage.getItem('G_PSID1');
|
||||
if (accessToken) {
|
||||
req.headers.Authorization = accessToken;
|
||||
}
|
||||
return req;
|
||||
})
|
||||
|
||||
client.interceptors.response.use(res => {
|
||||
if (res.status != 200) {
|
||||
return Promise.reject(new Error(`http response ${res.status}:${res.statusText}`))
|
||||
}
|
||||
if (typeof res.data === 'object' && res.data.hasOwnProperty('code')) {
|
||||
if (res.data['code'] === 0) {
|
||||
return res.data['data']
|
||||
} else {
|
||||
return Promise.reject(new HttpError(res.data['reason'], res.data['code']))
|
||||
}
|
||||
}
|
||||
if (typeof res.data === 'object' && res.data.hasOwnProperty('errno')) {
|
||||
if (res.data['errno'] == 0) {
|
||||
return res.data['result']
|
||||
} else {
|
||||
return Promise.reject(new HttpError(res.data['errmsg'], res.data['errno']))
|
||||
}
|
||||
}
|
||||
return res;
|
||||
})
|
||||
|
||||
export default client;
|
|
@ -0,0 +1,15 @@
|
|||
import request from './request'
|
||||
|
||||
/**
|
||||
* 连接到指定的endpoint
|
||||
* @param {String} e
|
||||
* @returns
|
||||
*/
|
||||
export function connectEndpoint(e) {
|
||||
return request.put(`/endpoint/connect/${e}`)
|
||||
}
|
||||
|
||||
|
||||
export function getConfigure() {
|
||||
return request.get(`/user/configures`)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
export function deepCopy(v1, v2, ps) {
|
||||
for (let k in v2) {
|
||||
if (ps && typeof ps === 'object' && Array.isArray(ps)) {
|
||||
if (ps.indexOf(k) == -1) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
let v = v2[k];
|
||||
if (typeof v === 'object') {
|
||||
if (!v1[k]) {
|
||||
v1[k] = Array.isArray(v) ? [] : {};
|
||||
}
|
||||
if (typeof v1[k] !== 'object') {
|
||||
continue
|
||||
}
|
||||
v1[k] = deepCopy(v1[k], v)
|
||||
} else {
|
||||
v1[k] = v
|
||||
}
|
||||
}
|
||||
return v1
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
export function durationFormat(duration, sep = null) {
|
||||
duration = parseInt(duration);
|
||||
if (isNaN(duration)) {
|
||||
return '';
|
||||
}
|
||||
var hour = parseInt(duration / 3600);
|
||||
var r = duration % 3600;
|
||||
var min = parseInt(r / 60);
|
||||
var sec = parseInt(r % 60);
|
||||
hour = isNaN(hour) ? 0 : hour;
|
||||
min = isNaN(min) ? 0 : min;
|
||||
sec = isNaN(sec) ? 0 : sec;
|
||||
if (sep) {
|
||||
hour = hour > 9 ? hour : '0' + hour
|
||||
min = min > 9 ? min : '0' + min
|
||||
sec = sec > 9 ? sec : '0' + sec
|
||||
return hour + sep + min + sep + sec;
|
||||
} else {
|
||||
let s = '';
|
||||
if (hour > 0) {
|
||||
s += hour + "h"
|
||||
}
|
||||
if (min > 0 || s != '') {
|
||||
s += min + "m"
|
||||
}
|
||||
s += sec + "s";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export function monryFormat(number, decimals = 2, decPoint = '.', thousandsSep = ',') {
|
||||
number = (number + '').replace(/[^0-9+-Ee.]/g, '')
|
||||
let n = !isFinite(+number) ? 0 : +number
|
||||
let prec = !isFinite(+decimals) ? 0 : Math.abs(decimals)
|
||||
let sep = (typeof thousandsSep === 'undefined') ? ',' : thousandsSep
|
||||
let dec = (typeof decPoint === 'undefined') ? '.' : decPoint
|
||||
let s = ''
|
||||
let toFixedFix = function (n, prec) {
|
||||
let k = Math.pow(10, prec)
|
||||
return '' + Math.ceil(n * k) / k
|
||||
}
|
||||
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.')
|
||||
let re = /(-?\d+)(\d{3})/
|
||||
while (re.test(s[0])) {
|
||||
s[0] = s[0].replace(re, '$1' + sep + '$2')
|
||||
}
|
||||
if ((s[1] || '').length < prec) {
|
||||
s[1] = s[1] || ''
|
||||
s[1] += new Array(prec - s[1].length + 1).join('0')
|
||||
}
|
||||
return s.join(dec)
|
||||
}
|
||||
|
||||
export function byteFormat(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
var k = 1024;
|
||||
let sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
let i = Math.floor(Math.log(bytes) / Math.log(k)) //return (bytes / Math.pow(k, i)) + ' ' + sizes[i];
|
||||
|
||||
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
import rawMenus from '@/config/menu'
|
||||
import pluralize from 'pluralize'
|
||||
import { deepCopy } from './deepcopy';
|
||||
|
||||
function capitalize(s) {
|
||||
return s.replace(/\b(\w)(\w*)/g, ($0, $1, $2) => {
|
||||
return $1.toUpperCase() + $2.toLowerCase();
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的菜单数据
|
||||
* @returns
|
||||
*/
|
||||
export function getMenus() {
|
||||
let cloneMenus = deepCopy(rawMenus)
|
||||
return cloneMenus
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化对象
|
||||
* @param {Object} menu
|
||||
* @returns
|
||||
*/
|
||||
export function nameize(menu) {
|
||||
let componentName = menu.name || '';
|
||||
if (componentName === '') {
|
||||
componentName = menu.route.substr(1).split('/').map(v => {
|
||||
return v[0] === ":" ? "" : capitalize(pluralize.singular(v));
|
||||
}).join('')
|
||||
}
|
||||
return componentName
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定路由的导航路径
|
||||
* @param {Array} values
|
||||
* @param {String} target
|
||||
*/
|
||||
export function findNavigation(values, target) {
|
||||
let paths = [];
|
||||
for (let i in values) {
|
||||
let row = values[i];
|
||||
if (row.route && row.route === target) {
|
||||
paths.push({
|
||||
navigation: row.navigation || false,
|
||||
label: row.label,
|
||||
route: row.route
|
||||
})
|
||||
return paths;
|
||||
}
|
||||
if (row.children && Array.isArray(row.children)) {
|
||||
let vs = findNavigation(row.children, target);
|
||||
if (Array.isArray(vs)) {
|
||||
paths.push({
|
||||
navigation: row.navigation || false,
|
||||
label: row.label,
|
||||
route: row.route
|
||||
});
|
||||
for (let j in vs) {
|
||||
paths.push(vs[j])
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过菜单配置构建菜单菜单路由组件
|
||||
* @param {Array} rows
|
||||
* @param {Object} components
|
||||
* @returns
|
||||
*/
|
||||
export function buildRoutes(rows, components) {
|
||||
let routes = [];
|
||||
for (let menu of rows) {
|
||||
let route = { name: '', path: menu.route || '/', meta: {}, component: null, props: {} }
|
||||
route.meta.title = menu.label || '';
|
||||
route.props.title = route.meta.title
|
||||
route.name = nameize(menu)
|
||||
route.meta.hidden = menu.hidden ? true : false
|
||||
route.meta.cache = menu.cache ? true : false
|
||||
if (menu.permissions) {
|
||||
route.meta.permissions = menu.permissions
|
||||
}
|
||||
let viewPath = menu.view;
|
||||
if (!viewPath) {
|
||||
let tokens = menu.route.substr(1).split('/').map((v) => {
|
||||
return v[0] === ":" ? "" : v;
|
||||
}).filter((v) => {
|
||||
return v !== "";
|
||||
})
|
||||
let totalLength = tokens.length;
|
||||
if (totalLength > 0) {
|
||||
if (totalLength == 1 || pluralize.isPlural(tokens[totalLength - 1])) {
|
||||
if (pluralize.isPlural(tokens[totalLength - 1])) {
|
||||
tokens[totalLength - 1] = pluralize.singular(tokens[totalLength - 1])
|
||||
}
|
||||
tokens.push('index');
|
||||
totalLength++;
|
||||
}
|
||||
tokens[totalLength - 1] = capitalize(tokens[totalLength - 1]);
|
||||
}
|
||||
viewPath = '../views/' + tokens.join("/") + ".vue"
|
||||
}
|
||||
if (components[viewPath]) {
|
||||
route.component = components[viewPath]
|
||||
}
|
||||
if (Array.isArray(menu.children)) {
|
||||
for (let child of buildRoutes(menu.children, components)) {
|
||||
if (!child.meta) {
|
||||
child.meta = {};
|
||||
}
|
||||
child.meta.children = true;
|
||||
child.meta.route = {
|
||||
parent: menu.route
|
||||
}
|
||||
routes.push(child);
|
||||
}
|
||||
}
|
||||
routes.push(route);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断该路由是否有权限访问
|
||||
* @param {Object} row
|
||||
* @param {Array} permissions
|
||||
* @returns
|
||||
*/
|
||||
function isAllowed(row, permissions) {
|
||||
if (row.access && row.access == 'allow') {
|
||||
return true
|
||||
}
|
||||
let name = nameize(row)
|
||||
if (permissions.indexOf(name) > -1) {
|
||||
return true
|
||||
}
|
||||
//只要有页面下面一个权限,那么就要显示该页面
|
||||
if (Array.isArray(row.permissions)) {
|
||||
for (let per of row.permissions) {
|
||||
if (permissions.indexOf(name + '.' + per)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
//只要有一个子项目有权限,那么他就有权限
|
||||
if (Array.isArray(row.children)) {
|
||||
for (let child of row.children) {
|
||||
if (isAllowed(child, permissions)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function filterMenusInner(items, permissions) {
|
||||
let data = [];
|
||||
for (let i in items) {
|
||||
let row = Object.assign({}, items[i]);
|
||||
if (!row.hidden) {
|
||||
if (isAllowed(row, permissions)) {
|
||||
if (Array.isArray(row.children)) {
|
||||
row.children = filterMenusInner(row.children, permissions)
|
||||
}
|
||||
data.push(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤有权限的菜单
|
||||
* @param {Array} permissions
|
||||
* @returns
|
||||
*/
|
||||
export function filterMenus(permissions) {
|
||||
return filterMenusInner(getMenus(), permissions)
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
|
||||
import Color from 'color'
|
||||
|
||||
let statusMap = [
|
||||
{
|
||||
"label": "空闲",
|
||||
"value": "idle",
|
||||
"color": "#198754",
|
||||
"retain": false
|
||||
},
|
||||
{
|
||||
"label": "忙碌",
|
||||
"value": "onBreak",
|
||||
"color": "#F09D00",
|
||||
"retain": false
|
||||
},
|
||||
{
|
||||
"label": "用餐",
|
||||
"value": "dining",
|
||||
"color": "#83261e",
|
||||
"retain": false
|
||||
},
|
||||
{
|
||||
"label": "休假",
|
||||
"value": "holiday",
|
||||
"color": "#b3261e",
|
||||
"retain": false
|
||||
},
|
||||
{
|
||||
"label": "振铃",
|
||||
"value": "ringing",
|
||||
"color": "#e63757",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "通话",
|
||||
"value": "answer",
|
||||
"color": "#b3261e",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "静音",
|
||||
"value": "muted",
|
||||
"color": "#34AB62",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "转接",
|
||||
"value": "transfer",
|
||||
"color": "#C8D541",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "耳语",
|
||||
"value": "whisper",
|
||||
"color": "#43539D",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "监听",
|
||||
"value": "eavesdrop",
|
||||
"color": "#0824F8",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "未注册",
|
||||
"value": "unavailable",
|
||||
"color": "#bbbbbb",
|
||||
"retain": true
|
||||
},
|
||||
{
|
||||
"label": "话后",
|
||||
"value": "wrapUp",
|
||||
"color": "#E36B09",
|
||||
"retain": true
|
||||
}
|
||||
]
|
||||
|
||||
const statusList = ['answer', 'transfer', 'whisper', 'eavesdrop', 'ringing', 'idle', 'onBreak'];
|
||||
|
||||
|
||||
export function updateStatusMap(x) {
|
||||
if (Array.isArray(x)) {
|
||||
statusMap = x;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusPosition(s) {
|
||||
let pos = statusList.indexOf(s);
|
||||
if (pos === -1) {
|
||||
pos = statusList.length + 1
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
export function getUserStatus() {
|
||||
return statusMap.filter(v => {
|
||||
return !v.retain;
|
||||
})
|
||||
}
|
||||
|
||||
export function getStatusText(s) {
|
||||
for (let i in statusMap) {
|
||||
let row = statusMap[i];
|
||||
if (row.value === s) {
|
||||
return row.label;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function getStatusTextColor(s) {
|
||||
for (let i in statusMap) {
|
||||
let row = statusMap[i];
|
||||
if (row.value === s) {
|
||||
return row.color;
|
||||
}
|
||||
}
|
||||
return '#1c1d21'
|
||||
}
|
||||
|
||||
export function getStatusBGColor(s) {
|
||||
let c = getStatusTextColor(s);
|
||||
return Color(c).fade(0.8).hsl()
|
||||
}
|
|
@ -0,0 +1,562 @@
|
|||
#app {
|
||||
--el-text-color-primary: #213547;
|
||||
--el-text-color-muted: #95aac9;
|
||||
--form-control-width: 210px;
|
||||
--box-shadow: 0px 0px 30px 8px rgba(0, 0, 0, 0.046);
|
||||
--placeholder-text-color: #c1ccdb;
|
||||
--form-control-border-color: #d2ddec;
|
||||
--el-color-available: #00d97e;
|
||||
--el-color-link: #0067b8;
|
||||
|
||||
.el-card {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
--el-dialog-border-radius: 6px;
|
||||
|
||||
.el-dialog__title {
|
||||
--el-dialog-title-font-size: .95rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.el-footer {
|
||||
--el-footer-padding: 0 20px;
|
||||
|
||||
.d-flex {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
border: none !important;
|
||||
--el-tag-border-radius: 6px;
|
||||
--el-tag-font-size: .7rem;
|
||||
|
||||
&.el-tag--primary {
|
||||
--el-tag-bg-color: var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
&.el-tag--success {
|
||||
--el-tag-bg-color: var(--el-color-success-light-8);
|
||||
}
|
||||
|
||||
&.el-tag--info {
|
||||
--el-tag-bg-color: var(--el-color-info-light-8);
|
||||
}
|
||||
|
||||
&.el-tag--warning {
|
||||
--el-tag-bg-color: var(--el-color-warning-light-8);
|
||||
}
|
||||
|
||||
&.el-tag--danger {
|
||||
--el-tag-bg-color: var(--el-color-danger-light-8);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input {
|
||||
--el-input-border-color: var(--form-control-border-color);
|
||||
|
||||
.el-input__inner {
|
||||
&::placeholder {
|
||||
font-size: .75rem;
|
||||
color: var(--placeholder-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.round {
|
||||
.el-input__wrapper {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-textarea {
|
||||
--el-input-border-color: var(--form-control-border-color);
|
||||
|
||||
.el-textarea__inner {
|
||||
&::placeholder {
|
||||
font-size: .75rem;
|
||||
color: var(--placeholder-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-range-input {
|
||||
&::placeholder {
|
||||
font-size: .75rem;
|
||||
color: var(--placeholder-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-date-editor {
|
||||
font-size: .8rem;
|
||||
--el-input-border-color: var(--form-control-border-color);
|
||||
}
|
||||
|
||||
.el-date-range-picker__time-header {
|
||||
.el-input {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-date-editor--datetimerange {
|
||||
/**
|
||||
* TODO 设置了这个值会导致点击框框后弹出的选择日期的组件错位,小于270像素会导致错位
|
||||
*/
|
||||
// --el-date-editor-datetimerange-width: calc( - 20px);
|
||||
--el-date-editor-datetimerange-width: calc(var(--form-control-width) - 16px);
|
||||
|
||||
&.el-input__wrapper {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.el-range-input {
|
||||
font-size: .8rem;
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-drawer {
|
||||
.el-drawer__body {
|
||||
--el-drawer-padding-primary: 0;
|
||||
}
|
||||
|
||||
.el-drawer__close {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
position: relative;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border-radius: inherit;
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(144, 147, 153, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
z-index: 1;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 10px;
|
||||
transition: opacity 0.12s ease-out;
|
||||
}
|
||||
|
||||
.px-1 {
|
||||
padding-left: .5rem;
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--el-text-color-muted);
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: var(--el-color-error);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--el-color-error);
|
||||
}
|
||||
|
||||
.flex-align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex;
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-fill {
|
||||
flex: 1 1 auto !important;
|
||||
|
||||
&.border-right {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&.border-left {
|
||||
border-left: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.flex-shrink {
|
||||
flex-shrink: 0;
|
||||
|
||||
&.border-right {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&.border-left {
|
||||
border-left: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.flex-horizontal,
|
||||
&.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&.flex-vertical,
|
||||
&.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// for segment element
|
||||
.segment-container {
|
||||
.segment-header {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0rem 0rem 1rem .5rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1.038rem;
|
||||
margin: 0;
|
||||
color: var(--el-text-color-primary);
|
||||
// color: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
|
||||
.segment-body {
|
||||
padding: 1.5rem 1rem;
|
||||
border-radius: 10px;
|
||||
background-color: var(--el-color-white);
|
||||
box-shadow: var(--box-shadow);
|
||||
|
||||
&.plain {
|
||||
padding: .5rem;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-toolbar {
|
||||
text-align: right;
|
||||
margin-bottom: .38rem;
|
||||
|
||||
.iconfont {
|
||||
margin-left: 1rem;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-muted);
|
||||
transition: all ease .5s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.segment-wrapper {
|
||||
.segment-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.el-descriptions {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.el-descriptions__content {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
span {
|
||||
cursor: pointer;
|
||||
font-size: .8rem;
|
||||
color: var(--el-text-color-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-item,
|
||||
.segment-dataview,
|
||||
.segment-descriptions {
|
||||
.el-tag {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
border: none;
|
||||
|
||||
.el-tag__content {
|
||||
font-size: .76rem;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
th.el-table__cell {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: #f9fbfd;
|
||||
}
|
||||
|
||||
td.el-table__cell {
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
.cell {
|
||||
a {
|
||||
color: var(--el-color-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.segment-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.segment-tag {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 1rem;
|
||||
line-height: 1;
|
||||
border-radius: 9999px;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
font-size: .76rem;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-descriptions {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.el-descriptions__label {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.is-bordered {
|
||||
.el-descriptions__label {
|
||||
display: table-cell;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.segment-form {
|
||||
|
||||
.el-form--inline {
|
||||
.el-form-item__label {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-textarea {
|
||||
width: auto !important;
|
||||
min-width: 80%;
|
||||
}
|
||||
|
||||
.el-upload-dragger {
|
||||
--el-upload-dragger-padding-horizontal: 1px;
|
||||
--el-upload-dragger-padding-vertical: 20px;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
.el-textarea {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: var(--form-control-width);
|
||||
|
||||
&.round {
|
||||
--el-input-border-radius: 30px;
|
||||
}
|
||||
|
||||
|
||||
.el-input__wrapper {
|
||||
transition: all .8s;
|
||||
padding: 1px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
}
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
background-color: rgb(242, 243, 245);
|
||||
transition: all .8s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
margin-left: .8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-action {
|
||||
display: inline-block;
|
||||
margin-right: .5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-plain {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
padding: 5px 0;
|
||||
align-items: center;
|
||||
|
||||
.avatar-smaples {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-plus {
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: var(--el-avatar-size);
|
||||
height: var(--el-avatar-size);
|
||||
border-radius: var(--el-avatar-size);
|
||||
background-color: var(--el-fill-color);
|
||||
cursor: pointer;
|
||||
transition: all .5s;
|
||||
margin-right: .5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-darker);
|
||||
}
|
||||
}
|
||||
|
||||
.el-avatar {
|
||||
margin-left: calc(-1* var(--el-avatar-size) / 4);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.avatar-container {
|
||||
.avatar-list {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.el-avatar{
|
||||
margin-right: .25rem;
|
||||
margin-bottom: .25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.avatar-footer{
|
||||
text-align: right;
|
||||
font-size: .8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.invisible-form {
|
||||
.el-button {
|
||||
--el-button-bg-color: transparent;
|
||||
--el-button-border-color: transparent;
|
||||
--el-button-hover-bg-color: transparent;
|
||||
--el-button-hover-border-color: transparent;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
--el-input-border-color: transparent;
|
||||
--el-select-input-focus-border-color: transparent;
|
||||
--el-select-border-color-hover: transparent;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
--el-input-border-color: transparent !important;
|
||||
--el-input-bg-color: transparent;
|
||||
--el-input-icon-color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#app {
|
||||
.el-main {
|
||||
--el-main-padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
@forward 'element-plus/theme-chalk/src/common/var.scss' with ($colors: ('primary': ('base': #0b57d0,
|
||||
),
|
||||
'success': ('base': #00d97e,
|
||||
),
|
||||
'warning':('base':#F09D00,
|
||||
),
|
||||
'danger':('base':#e63757,
|
||||
),
|
||||
'error':('base':#b3261e,
|
||||
),
|
||||
'info':('base':#6e84a3,
|
||||
)),
|
||||
);
|
||||
|
||||
@use "element-plus/theme-chalk/src/index.scss" as *;
|
||||
|
||||
@import 'element-plus/theme-chalk/display.css';
|
||||
|
||||
|
||||
.el-cascader {
|
||||
.el-input {
|
||||
.icon-arrow-down {
|
||||
&::before {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,296 @@
|
|||
<template>
|
||||
<div class="segment-form" ref="activeformElement">
|
||||
<el-form :model="activeModel" :label-width="columnLabelWidth" :rules="rules" :validate-on-rule-change="false"
|
||||
:inline="isInline" ref="activeform" status-icon>
|
||||
|
||||
<slot name="container" :model="activeModel" :schemas="visibleColumns">
|
||||
<el-row :gutter="20" v-if="grid">
|
||||
<el-col :span="getColSpan(schema)" v-for="(schema, index) in displayColumns" :key="index">
|
||||
<el-form-item :prop="schema.column" :label="schema.label" :error="getFieldError(schema)">
|
||||
<slot name="default" :model="activeModel" :schema="schema">
|
||||
<form-item :model="activeModel" :schema="schema" :scenario="scenario"></form-item>
|
||||
</slot>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template v-else>
|
||||
<el-form-item v-for="(schema, index) in displayColumns" :key="index" :prop="schema.column"
|
||||
:label="schema.label" :error="getFieldError(schema)">
|
||||
<slot name="default" :model="activeModel" :schema="schema">
|
||||
<form-item :model="activeModel" :schema="schema" :scenario="scenario"></form-item>
|
||||
</slot>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item v-if="actions.length > 0">
|
||||
<template v-for="action in actions">
|
||||
<action :action="action" @click="handleSubmit"></action>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</slot>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
||||
import { encode, decode } from './libs/codec'
|
||||
import FormItem from './parts/FormItem.vue';
|
||||
import Action from './parts/Action.vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { generateSchemaRule, checkSchemaVisible } from './libs/form'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
},
|
||||
schemas: {
|
||||
type: Array,
|
||||
},
|
||||
scenario: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
},
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
model: {
|
||||
type: Object
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
grid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
gridColumn: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
actions: {
|
||||
type: Array
|
||||
},
|
||||
autoCommit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
|
||||
const activeform = ref(null);
|
||||
|
||||
const activeformElement = ref(null);
|
||||
|
||||
const activeModel = ref({});
|
||||
|
||||
const formWidth = ref(window.innerWidth);
|
||||
|
||||
/**
|
||||
* 显示的列的schema
|
||||
*/
|
||||
const visibleColumns = ref([])
|
||||
|
||||
/**
|
||||
* 排除的列
|
||||
*/
|
||||
const excludeColumns = ref([])
|
||||
|
||||
const displayColumns = computed(() => {
|
||||
return visibleColumns.value.filter((v) => {
|
||||
return !v.attribute.invisible;
|
||||
})
|
||||
})
|
||||
|
||||
const isInline = computed(() => {
|
||||
if (props.grid) {
|
||||
return false;
|
||||
}
|
||||
return props.inline
|
||||
})
|
||||
|
||||
const columnLabelWidth = computed(() => {
|
||||
if (props.inline) {
|
||||
return 0
|
||||
} else {
|
||||
if (props.labelWidth) {
|
||||
return props.labelWidth
|
||||
} else {
|
||||
let width = formWidth.value;
|
||||
if (width < 768) {
|
||||
return '80px'
|
||||
} else {
|
||||
return '120px'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
let rules = {}
|
||||
if (props.scenario === 'search') {
|
||||
return rules;
|
||||
}
|
||||
for (let schema of visibleColumns.value) {
|
||||
rules[schema.column] = generateSchemaRule(schema, props.scenario)
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
const actions = computed(() => {
|
||||
return props.actions || []
|
||||
})
|
||||
|
||||
const getColSpan = (schema) => {
|
||||
if (props.gridColumn > 0) {
|
||||
return props.gridColumn
|
||||
}
|
||||
if (schema.format == 'textarea') {
|
||||
return 24;
|
||||
}
|
||||
let width = formWidth.value;
|
||||
if (width < 768) {
|
||||
return 24;
|
||||
}
|
||||
if (width < 960) {
|
||||
return 12;
|
||||
}
|
||||
return 8;
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
visibleColumns.value = props.schemas.filter(schema => {
|
||||
if (excludeColumns.value.indexOf(schema.column) > -1) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(schema.scenarios)) {
|
||||
if (schema.scenarios.indexOf(props.scenario) > -1) {
|
||||
//如果级联字段是生成多个字段,那么隐藏其他字段
|
||||
if (schema.format === 'cascader') {
|
||||
let columns = schema.attribute.live.columns;
|
||||
if (Array.isArray(columns)) {
|
||||
for (let i in columns) {
|
||||
if (columns[i] !== schema.column) {
|
||||
excludeColumns.value.push(columns[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return checkSchemaVisible(schema, activeModel.value)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
// 处理搜索的场景
|
||||
let model = props.model || {};
|
||||
if (props.scenario === 'search') {
|
||||
for (let i in visibleColumns.value) {
|
||||
let schema = visibleColumns.value[i];
|
||||
if (route.query.hasOwnProperty(schema.column)) {
|
||||
model[schema.column] = route.query[schema.column];
|
||||
}
|
||||
}
|
||||
}
|
||||
activeModel.value = decode(model, props.schemas, props.scenario)
|
||||
if (props.autoCommit) {
|
||||
nextTick(() => {
|
||||
submit().then(res => {
|
||||
for (let i in props.actions) {
|
||||
let action = props.actions[i];
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(res, visibleColumns.value);
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
formWidth.value = activeformElement.value.offsetWidth;
|
||||
//监控如果模型改变了,也对应刷新数据
|
||||
watch(() => props.model, (val) => {
|
||||
activeModel.value = decode(val, props.schemas, props.scenario)
|
||||
}, { deep: true })
|
||||
})
|
||||
|
||||
/**
|
||||
* 监控模型的值,动态调整字段显示操作
|
||||
*/
|
||||
watch(activeModel, () => {
|
||||
if (props.scenario !== 'search') {
|
||||
visibleColumns.value = props.schemas.filter(schema => {
|
||||
if (excludeColumns.value.indexOf(schema.column) > -1) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(schema.scenarios)) {
|
||||
if (schema.scenarios.indexOf(props.scenario) > -1) {
|
||||
return checkSchemaVisible(schema, activeModel.value)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
const getFieldError = (schema) => {
|
||||
//搜索场景不显示错误信息
|
||||
if (props.scenario === 'search') {
|
||||
return ''
|
||||
}
|
||||
return schema.error || ''
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let model = encode(activeModel.value, visibleColumns.value, props.scenario);
|
||||
//把主键的值原封不动的传递过去
|
||||
let primaryKeySchemas = props.schemas.filter(v => {
|
||||
return v.attribute.primary_key
|
||||
})
|
||||
if (primaryKeySchemas.length > 0) {
|
||||
for (let schema of primaryKeySchemas) {
|
||||
if (props.model && props.model.hasOwnProperty(schema.column)) {
|
||||
model[schema.column] = props.model[schema.column]
|
||||
}
|
||||
}
|
||||
}
|
||||
//判断表单是否验证失败
|
||||
activeform.value.validate().then(res => {
|
||||
emit('submit', model, visibleColumns.value)
|
||||
resolve(model)
|
||||
}).catch(e => {
|
||||
let errors = null;
|
||||
if (typeof e === 'object') {
|
||||
for (let i in e) {
|
||||
errors = e[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
reject(new Error(errors[0].message))
|
||||
} else {
|
||||
reject(new Error('validate error'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = (action) => {
|
||||
if (typeof action.callback === 'function') {
|
||||
submit().then(res => {
|
||||
action.callback(res, visibleColumns.value);
|
||||
}).catch(e => { console.log(e.message) })
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ submit })
|
||||
|
||||
</script>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="segment-descriptions">
|
||||
<el-descriptions :column="column" :border="border">
|
||||
<template #title>
|
||||
<slot name="title" :model="model"></slot>
|
||||
</template>
|
||||
<template #extra>
|
||||
<slot name="extra" :model="model"></slot>
|
||||
</template>
|
||||
<el-descriptions-item :label="schema.label" v-for="schema in visibleSchemas" :span="getSpan(schema)">
|
||||
<slot name="default" :model="model" :schema="schema">
|
||||
<cell :schema="schema" :model="model"></cell>
|
||||
</slot>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="segment-descriptions_footer">
|
||||
<slot name="footer" :model="model"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Cell from './parts/Cell.vue';
|
||||
import { checkSchemaVisible } from './libs/form';
|
||||
import { modelValueOf } from './libs/model';
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
},
|
||||
schemas: {
|
||||
type: Array,
|
||||
},
|
||||
scenario: {
|
||||
type: String,
|
||||
default: 'view',
|
||||
},
|
||||
model: {
|
||||
type: Object
|
||||
},
|
||||
column: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const getSpan = (schema) => {
|
||||
if (['textarea'].indexOf(schema.format) > -1) {
|
||||
return props.column;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const visibleSchemas = computed(() => {
|
||||
let modelValue = modelValueOf(props.model);
|
||||
return props.schemas.filter(v => {
|
||||
if (Array.isArray(v.scenarios)) {
|
||||
if (v.scenarios.indexOf(props.scenario) > -1) {
|
||||
return checkSchemaVisible(v, modelValue);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<el-table :data="models" :size="size" :border="true" :loading="loading" @selection-change="handleSelectionChange"
|
||||
@sort-change="handleSortableChange" @header-dragend="handleHeaderDragend">
|
||||
<el-table-column v-if="selection" type="selection" width="55"></el-table-column>
|
||||
<el-table-column v-for="(schema, index) in visibleSchemas" :key="index" :prop="schema.column" :label="schema.label"
|
||||
:sortable="schema.attribute.sort ? 'custom' : false" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<slot :model="scope.row" :schema="schema">
|
||||
<cell :model="scope.row" :schema="schema"></cell>
|
||||
</slot>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="actions.length > 0" fixed='right' class-name="segment-gridview-actions">
|
||||
<template #default="scope">
|
||||
<template v-for="action in actions">
|
||||
<action :action="action" v-if="isVisible(action, scope.row)" @click="handleSubmit(action, scope.row)">
|
||||
</action>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty />
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import Action from './parts/Action.vue';
|
||||
import Cell from './parts/Cell.vue';
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String
|
||||
},
|
||||
schemas: {
|
||||
type: Array
|
||||
},
|
||||
scenario: {
|
||||
type: String
|
||||
},
|
||||
selection: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
models: {
|
||||
type: Array
|
||||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const visibleSchemas = computed(() => {
|
||||
return props.schemas.filter(v => {
|
||||
if (Array.isArray(v.scenarios)) {
|
||||
if (v.scenarios.indexOf(props.scenario) > -1) {
|
||||
return !v.attribute.invisible;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(['selection', 'sort', 'dragend'])
|
||||
|
||||
const isVisible = (action, model) => {
|
||||
if (typeof action !== 'object') {
|
||||
return true;
|
||||
}
|
||||
if (!action.hasOwnProperty('hidden')) {
|
||||
return true;
|
||||
}
|
||||
let v = action['hidden'];
|
||||
if (typeof v === 'boolean') {
|
||||
return !v
|
||||
}
|
||||
if (typeof v === 'function') {
|
||||
return !v(model)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const handleSubmit = (action, model) => {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(model, props.schemas)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (e) => {
|
||||
emit('selection', e)
|
||||
}
|
||||
|
||||
const handleSortableChange = (e) => {
|
||||
emit('sort', e)
|
||||
}
|
||||
|
||||
const handleHeaderDragend = () => {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="segment-header d-flex">
|
||||
<div class="flex-fill">
|
||||
<slot name="headerleft">
|
||||
</slot>
|
||||
</div>
|
||||
<div class="flex-shrink">
|
||||
<slot name="headerright">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<el-skeleton :rows="8" :throttle="200" :loading="loading" animated>
|
||||
<template #default>
|
||||
<slot name="default" :crud="crud"></slot>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { onMounted, ref, nextTick } from 'vue';
|
||||
import CRUD from './libs/crud'
|
||||
|
||||
const props = defineProps({
|
||||
moduleName: {
|
||||
type: String,
|
||||
},
|
||||
tableName: {
|
||||
type: String,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const crud = ref(new CRUD({
|
||||
moduleName: props.moduleName,
|
||||
tableName: props.tableName,
|
||||
apiPrefix: ''
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true
|
||||
crud.value.initialize().then(res => {
|
||||
nextTick(() => {
|
||||
loading.value = false
|
||||
})
|
||||
emit('ready', crud)
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,592 @@
|
|||
<template>
|
||||
<skeleton :module-name="moduleName" :table-name="tableName" @ready="handleReady">
|
||||
<template #default>
|
||||
<div class="segment-container" v-if="isReady">
|
||||
<div v-if="!disableHeader" class="segment-header d-flex">
|
||||
<div class="flex-fill">
|
||||
<slot name="headerleft">
|
||||
<h3 v-if="!displayNavigation">{{ title }}</h3>
|
||||
<el-breadcrumb :separator-icon="ArrowRight" v-else>
|
||||
<template v-for="item in breadcrumbs">
|
||||
<el-breadcrumb-item v-if="item.navigation" :to="item.route">
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-else>{{ item.label }}</el-breadcrumb-item>
|
||||
</template>
|
||||
</el-breadcrumb>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="flex-shrink">
|
||||
<slot name="headerright">
|
||||
<el-button type="primary" round @click="handleCreate"
|
||||
v-if="hasPermission('create') && !readonly">新建</el-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="segment-body" :class="disableHeader ? 'plain' : ''">
|
||||
<div class="segment-search">
|
||||
<active-form :schemas="schemas" :size="size" scenario="search" :model="searchModel" :inline="true"
|
||||
:actions="searchButtons" :auto-commit="autoQuery">
|
||||
<template #default="{ model, schema }">
|
||||
<slot name="searchform" :model="model" :schema="schema"></slot>
|
||||
</template>
|
||||
</active-form>
|
||||
</div>
|
||||
<div class="segment-toolbar" v-if="!disableToolbar">
|
||||
<el-dropdown placement="bottom-end">
|
||||
<icon name="set"></icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<template v-for="action in toolbarButtons">
|
||||
<el-dropdown-item
|
||||
v-if="(!action.selection || (action.selection && selections.length > 0))"
|
||||
@click="handleToolbarAction(action)">{{ action.label }}</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="segment-dataview" v-loading="searching">
|
||||
<grid-view :schemas="schemas" :size="size" scenario="list" :models="models"
|
||||
:actions="gridviewButtons" @selection="handleSelectionChange" @sort="handleSortChange">
|
||||
<template #default="{ model, schema }">
|
||||
<slot name="gridview" :model="model" :schema="schema"></slot>
|
||||
</template>
|
||||
</grid-view>
|
||||
<el-pagination :page-size="pageSize" layout="total, prev, pager, next" :total="totalCount"
|
||||
@current-change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else></el-empty>
|
||||
<template v-if="formMode === 'drawer'">
|
||||
<el-drawer v-model="formVisible" :title="formTitle" :size="formWidth" :destroy-on-close="true"
|
||||
:show-close="true">
|
||||
<template #default>
|
||||
<active-form :schemas="schemas" :size="size" :scenario="formScenario" :model="formModel"
|
||||
:actions="formButtons">
|
||||
<template #default="{ model, schema }">
|
||||
<slot name="crudform" :model="model" :schema="schema"></slot>
|
||||
</template>
|
||||
</active-form>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-dialog v-model="formVisible" :title="formTitle" :width="formWidth" draggable :destroy-on-close="true">
|
||||
<active-form :schemas="schemas" :size="size" :scenario="formScenario" :model="formModel"
|
||||
:actions="formButtons">
|
||||
<template #default="{ model, schema }">
|
||||
<slot name="crudform" :model="model" :schema="schema"></slot>
|
||||
</template>
|
||||
</active-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</template>
|
||||
</skeleton>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Skeleton from './Skeleton.vue';
|
||||
import ActiveForm from './ActiveForm.vue';
|
||||
import GridView from './GridView.vue';
|
||||
import CRUD from './libs/crud'
|
||||
import Icon from '@/components/widgets/Icon.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { clearSearchModel } from './libs/form'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import ValidateError from './libs/error'
|
||||
import { useRoute } from 'vue-router';
|
||||
import useUserStore from '@/stores/user'
|
||||
import { findNavigation, getMenus } from '@/assets/js/menu';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
/**
|
||||
* @type {CRUD}
|
||||
*/
|
||||
var crud;
|
||||
|
||||
/**
|
||||
* 是否已就绪
|
||||
*/
|
||||
const isReady = ref(false)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const breadcrumbs = ref([]);
|
||||
/**
|
||||
* 定义属性
|
||||
*/
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
moduleName: {
|
||||
type: String
|
||||
},
|
||||
tableName: {
|
||||
type: String
|
||||
},
|
||||
formMode: { //表单渲染模式,有 [drawer,dialog,target]
|
||||
type: String,
|
||||
default: 'dialog',
|
||||
},
|
||||
disableHeader: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableToolbar: { //禁用按钮
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
searchActions: { //搜索的按钮
|
||||
type: Array,
|
||||
},
|
||||
formActions: { //表单里面的按钮
|
||||
type: Array,
|
||||
},
|
||||
gridviewActions: { //表格里面的按钮
|
||||
type: Array,
|
||||
},
|
||||
toolbarActions: {
|
||||
type: Array,
|
||||
},
|
||||
disablePermission: { //禁用缓存
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
defaultSortable: { //默认排序规则
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
defaultQuery: { //默认的搜索项目
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
fixedQueue: { //固定的搜索项目
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
displayNavigation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoQuery: {
|
||||
type: Boolean, //进入页面后自动查询
|
||||
default: true
|
||||
},
|
||||
beforeCreate: {
|
||||
type: Function,
|
||||
default: (model, schemas) => {
|
||||
return model
|
||||
}
|
||||
},
|
||||
beforeUpdate: {
|
||||
type: Function,
|
||||
default: (model, schemas) => {
|
||||
return model
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const formVisible = ref(false)
|
||||
|
||||
const formScenario = ref('create')
|
||||
|
||||
const searchModel = ref({});
|
||||
|
||||
const formModel = ref({});
|
||||
|
||||
const selections = ref([]);
|
||||
|
||||
const searching = ref(false);
|
||||
|
||||
const formWidth = computed(() => {
|
||||
let width = window.innerWidth
|
||||
if (width < 768) {
|
||||
return '96%'
|
||||
} else if (width <= 1180) {
|
||||
return '80%'
|
||||
} else if (width <= 1366) {
|
||||
return '60%'
|
||||
} else {
|
||||
return '40%'
|
||||
}
|
||||
})
|
||||
|
||||
const formTitle = computed(() => {
|
||||
let title = props.title;
|
||||
if (typeof title !== 'string') {
|
||||
return;
|
||||
}
|
||||
let skips = ['列表', '管理'];
|
||||
let length = title.length;
|
||||
for (let s of skips) {
|
||||
if (title.indexOf(s) == length - s.length) {
|
||||
title = title.substring(0, length - s.length);
|
||||
}
|
||||
}
|
||||
let actionText = formScenario.value === 'create' ? '新建' : '更新'
|
||||
return actionText + (title || '')
|
||||
})
|
||||
|
||||
const hasPermission = (s) => {
|
||||
if (props.disablePermission) {
|
||||
return true
|
||||
}
|
||||
let ps = s;
|
||||
if (ps.indexOf('.') === -1) {
|
||||
ps = route.name + '.' + s;
|
||||
}
|
||||
const { hasPermission } = useUserStore();
|
||||
return hasPermission(ps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的表结构信息
|
||||
*/
|
||||
const schemas = computed(() => {
|
||||
return isReady.value ? crud.value.getSchemas() : []
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取所有查询信息
|
||||
*/
|
||||
const models = computed(() => {
|
||||
return isReady.value ? crud.value.getModels() : []
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取分页大小
|
||||
*/
|
||||
const pageSize = computed(() => {
|
||||
let pagesize = isReady.value ? crud.value.getPaginationSize() : 10
|
||||
return pagesize
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取条目总数
|
||||
*/
|
||||
const totalCount = computed(() => {
|
||||
return isReady.value ? crud.value.getPaginationCount() : 1
|
||||
})
|
||||
|
||||
/**
|
||||
* 默认搜索的动作
|
||||
*/
|
||||
const defaultSearchActions = {
|
||||
'search': {
|
||||
name: 'search',
|
||||
label: '查询',
|
||||
type: 'primary',
|
||||
callback: (model, schemas) => {
|
||||
searching.value = true
|
||||
crud.value.resetPagination().setQueryParams(clearSearchModel(model, schemas)).searchModel().then(res => {
|
||||
searching.value = false
|
||||
}).catch(e => {
|
||||
searching.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultToolbarActions = {
|
||||
'export': {
|
||||
name: 'export',
|
||||
label: '导出数据',
|
||||
type: 'primary',
|
||||
permission: 'export',
|
||||
callback: (values) => {
|
||||
crud.value.exportModels().then(res => {
|
||||
|
||||
}).catch(e => {
|
||||
ElMessage.error(`导出失败: ${e.message}`)
|
||||
})
|
||||
}
|
||||
},
|
||||
'batchDelete': {
|
||||
name: 'batchDelete',
|
||||
label: '删除数据',
|
||||
type: 'danger',
|
||||
selection: true,
|
||||
permission: 'delete',
|
||||
callback: (values) => {
|
||||
crud.value.deleteModels(values).then(res => {
|
||||
if (res.total > res.success) {
|
||||
ElMessage.success(`删除成功${res.success}条,失败${res.total - res.success}条`)
|
||||
} else {
|
||||
ElMessage.success(`删除成功`)
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error(`删除失败: ${e.message}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认表单的动作
|
||||
*/
|
||||
const defaultFormActions = {
|
||||
'save': {
|
||||
name: 'save',
|
||||
label: '保存',
|
||||
type: 'primary',
|
||||
callback: (model, schemas) => {
|
||||
let ps = null;
|
||||
crud.value.resetError()
|
||||
if (formScenario.value === 'create') {
|
||||
if (typeof props.beforeCreate === 'function') {
|
||||
model = props.beforeCreate(model, schemas);
|
||||
}
|
||||
ps = crud.value.createModel(model)
|
||||
} else {
|
||||
if (typeof props.beforeUpdate === 'function') {
|
||||
model = props.beforeUpdate(model, schemas);
|
||||
}
|
||||
ps = crud.value.updateModel(model)
|
||||
}
|
||||
ps.then(res => {
|
||||
ElMessage.success("保存成功")
|
||||
formVisible.value = false
|
||||
}).catch(e => {
|
||||
if (e instanceof ValidateError) {
|
||||
crud.value.setColumnError(e.schema.column, e.schema.message)
|
||||
} else {
|
||||
ElMessage.error(`保存失败: ${e.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认表格的动作
|
||||
*/
|
||||
const defaultGridviewActions = {
|
||||
'edit': {
|
||||
name: 'edit',
|
||||
label: '编辑',
|
||||
type: 'success',
|
||||
icon: 'edit',
|
||||
permission: 'update',
|
||||
callback: (model, schemas) => {
|
||||
let qs = {
|
||||
'scenario': 'update',
|
||||
'__format': 'raw',
|
||||
}
|
||||
qs[crud.value.primaryKey] = crud.value.findModelPrimaryKey(model)
|
||||
crud.value.getModel(qs).then(res => {
|
||||
formScenario.value = 'update'
|
||||
formModel.value = res
|
||||
formVisible.value = true
|
||||
}).catch(e => {
|
||||
ElMessage.error(`获取数据失败: ${e.message}`)
|
||||
})
|
||||
}
|
||||
},
|
||||
'delete': {
|
||||
name: 'delete',
|
||||
label: '删除',
|
||||
type: 'danger',
|
||||
icon: 'ashbin',
|
||||
permission: 'delete',
|
||||
callback: (model, schemas) => {
|
||||
ElMessageBox.confirm(
|
||||
'您确定要删除该条目吗?',
|
||||
'温馨提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(res => {
|
||||
crud.value.deleteModel(model).then(res => {
|
||||
ElMessage.success("删除成功")
|
||||
}).catch(e => {
|
||||
ElMessage.error(`删除失败: ${e.message}`)
|
||||
})
|
||||
}).catch(e => { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mergeButtons = (actions, defaults) => {
|
||||
let buttons = [];
|
||||
if (!Array.isArray(actions)) {
|
||||
for (let i in defaults) {
|
||||
let action = defaults[i];
|
||||
if (props.readonly && ['edit', 'delete'].indexOf(action['name']) > -1) {
|
||||
continue
|
||||
}
|
||||
if (action['permission']) {
|
||||
if (hasPermission(action['permission'])) {
|
||||
buttons.push(action)
|
||||
}
|
||||
} else {
|
||||
buttons.push(action)
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
for (let i in actions) {
|
||||
let action = actions[i]
|
||||
if (typeof action === 'string') {
|
||||
if (!defaults[action]) {
|
||||
continue
|
||||
}
|
||||
action = defaults[action]
|
||||
}
|
||||
if (typeof action !== 'object') {
|
||||
continue
|
||||
}
|
||||
if (props.readonly && ['edit', 'delete'].indexOf(action['name']) > -1) {
|
||||
continue
|
||||
}
|
||||
if (action['permission']) {
|
||||
if (hasPermission(action['permission'])) {
|
||||
buttons.push(action)
|
||||
}
|
||||
} else {
|
||||
buttons.push(action)
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询的按钮渲染
|
||||
*/
|
||||
const searchButtons = computed(() => {
|
||||
return mergeButtons(props.searchActions, defaultSearchActions)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* 数据列表的按钮渲染
|
||||
*/
|
||||
const gridviewButtons = computed(() => {
|
||||
return mergeButtons(props.gridviewActions, defaultGridviewActions)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* 表单的按钮渲染
|
||||
*/
|
||||
const formButtons = computed(() => {
|
||||
return mergeButtons(props.formActions, defaultFormActions)
|
||||
})
|
||||
|
||||
const toolbarButtons = computed(() => {
|
||||
return mergeButtons(props.toolbarActions, defaultToolbarActions);
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理创建事件
|
||||
* @param {any} e
|
||||
*/
|
||||
const handleCreate = (e) => {
|
||||
formScenario.value = 'create'
|
||||
formVisible.value = true
|
||||
formModel.value = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化完成回调
|
||||
* @param {CRUD} e
|
||||
*/
|
||||
const handleReady = (e) => {
|
||||
crud = e
|
||||
isReady.value = true;
|
||||
if (props.defaultSortable != '') {
|
||||
if (props.defaultSortable.indexOf('-') == 0) {
|
||||
crud.value.setSortable(props.defaultSortable.substring(1), 'descending')
|
||||
} else {
|
||||
crud.value.setSortable(props.defaultSortable, 'ascending')
|
||||
}
|
||||
}
|
||||
if (typeof props.fixedQueue === 'object') {
|
||||
for (let k in props.fixedQueue) {
|
||||
crud.value.addFixedQueueParams(k, props.fixedQueue[k]);
|
||||
}
|
||||
}
|
||||
if (typeof props.defaultQuery === 'object') {
|
||||
for (let k in props.defaultQuery) {
|
||||
searchModel.value[k] = props.defaultQuery[k];
|
||||
}
|
||||
}
|
||||
//crud.value.searchModel().then(res => { }).catch(e => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分页改变
|
||||
* @param {*} e
|
||||
*/
|
||||
const handlePageChange = (e) => {
|
||||
searching.value = true
|
||||
crud.value.setPaginationIndex(e).searchModel().then(res => {
|
||||
searching.value = false
|
||||
}).catch(e => {
|
||||
searching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理选中值改变
|
||||
* @param {*} e
|
||||
*/
|
||||
const handleSelectionChange = (e) => {
|
||||
selections.value = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理排序改变
|
||||
* @param {*} e
|
||||
*/
|
||||
const handleSortChange = (e) => {
|
||||
searching.value = true
|
||||
crud.value.setSortable(e.prop, e.order).searchModel().then(res => {
|
||||
searching.value = false
|
||||
}).catch(e => {
|
||||
searching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const handleToolbarAction = (a) => {
|
||||
if (typeof a.callback === 'function') {
|
||||
a.callback(selections.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.displayNavigation) {
|
||||
let navs = findNavigation(getMenus(), route.path);
|
||||
let pos = -1;
|
||||
for (let i in navs) {
|
||||
if (navs[i].route == route.path) {
|
||||
pos = i;
|
||||
}
|
||||
}
|
||||
if (pos > -1) {
|
||||
breadcrumbs.value = navs.splice(pos - 1, 2);
|
||||
} else {
|
||||
breadcrumbs.value = navs;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div class="segment-container segment-wrapper">
|
||||
<div class="segment-header d-flex">
|
||||
<div class="flex-fill">
|
||||
<slot name="headerleft">
|
||||
<el-breadcrumb :separator-icon="ArrowRight" v-if="navigation">
|
||||
<template v-for="item in breadcrumbs">
|
||||
<el-breadcrumb-item v-if="item.navigation" :to="item.route">
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-else>{{ item.label }}</el-breadcrumb-item>
|
||||
</template>
|
||||
</el-breadcrumb>
|
||||
<h3 v-else>
|
||||
{{ title }}
|
||||
</h3>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="flex-shrink">
|
||||
<slot name="headerright">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="segment-body" :class="plain ? 'plain' : ''">
|
||||
<slot name="default">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { findNavigation, getMenus } from '@/assets/js/menu';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
navigation: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fullNavigation: { //是否使用全路径
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
plain:{
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const breadcrumbs = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
let navs = findNavigation(getMenus(), route.path);
|
||||
if (props.fullNavigation) {
|
||||
breadcrumbs.value = navs;
|
||||
} else {
|
||||
let pos = -1;
|
||||
for (let i in navs) {
|
||||
if (navs[i].route == route.path) {
|
||||
pos = i;
|
||||
}
|
||||
}
|
||||
if (pos > -1) {
|
||||
breadcrumbs.value = navs.splice(pos - 1, 2);
|
||||
} else {
|
||||
breadcrumbs.value = navs;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,238 @@
|
|||
import dayjs from "dayjs"
|
||||
|
||||
function mustMarshal(v, s) {
|
||||
let value = null
|
||||
switch (s) {
|
||||
case 'integer':
|
||||
if (v instanceof Date) {
|
||||
value = v.getTime() / 1000
|
||||
} else {
|
||||
value = parseInt(v)
|
||||
value = isNaN(value) ? 0 : value
|
||||
}
|
||||
break
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
value = parseFloat(v)
|
||||
value = isNaN(value) ? 0 : value
|
||||
break
|
||||
case 'boolean':
|
||||
value = Boolean(v)
|
||||
break
|
||||
case 'string':
|
||||
if (v instanceof Date) {
|
||||
value = dayjs(v).format("YYYY-MM-DD HH:mm:ss")
|
||||
} else if (typeof v === 'object') {
|
||||
value = JSON.stringify(v)
|
||||
} else {
|
||||
value = v + ''
|
||||
}
|
||||
break
|
||||
default:
|
||||
value = v
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 把对象转换成时间对象
|
||||
* @param {Object} v
|
||||
* @returns
|
||||
*/
|
||||
export function toDate(v) {
|
||||
if (typeof v === 'string') {
|
||||
return dayjs(v, ['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD']).toDate();
|
||||
} else if (typeof v === 'number') {
|
||||
if (v / 1000000000 < 1000) {
|
||||
v = v * 1000;
|
||||
}
|
||||
return new Date(v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型数据编码为数据库数
|
||||
* @param {Object} raw
|
||||
* @param {Array} schemas
|
||||
* @returns
|
||||
*/
|
||||
export function encode(raw, schemas, scenario) {
|
||||
let data = {}
|
||||
let excludes = []
|
||||
if (!raw || !schemas) {
|
||||
return data
|
||||
}
|
||||
scenario = scenario || 'unknown';
|
||||
for (let schema of schemas) {
|
||||
if (schema.attribute.live.enable && schema.attribute.live.type == 'cascader') {
|
||||
let columns = schema.attribute.live.columns
|
||||
//级联数据如果没有配置字段,那么对结果进行JSON编码处理
|
||||
if (!Array.isArray(columns) || columns.length == 0) {
|
||||
data[schema.column] = JSON.stringify(raw[schema.column])
|
||||
} else {
|
||||
let vals = raw[schema.column]
|
||||
if (vals && Array.isArray(vals)) {
|
||||
for (let i in vals) {
|
||||
if (columns[i]) {
|
||||
//级联数据所有数据都必须是同一个类型的数据
|
||||
data[columns[i]] = mustMarshal(vals[i], schema.type)
|
||||
excludes.push(columns[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (['multiSelect'].indexOf(schema.format) > -1) {
|
||||
try {
|
||||
if (typeof raw[schema.column] === 'object') {
|
||||
data[schema.column] = mustMarshal(
|
||||
JSON.stringify(raw[schema.column]),
|
||||
schema.type
|
||||
)
|
||||
}
|
||||
} catch (e) { }
|
||||
} else if (['date', 'datetime', 'timestamp'].indexOf(schema.format) > -1) {
|
||||
let value = raw[schema.column];
|
||||
if (Array.isArray(value)) {
|
||||
let ps = []
|
||||
for (let i in value) {
|
||||
let row = value[i];
|
||||
ps.push(mustMarshal(toDate(row), schema.type))
|
||||
}
|
||||
data[schema.column] = ps.join('/')
|
||||
} else {
|
||||
data[schema.column] = mustMarshal(toDate(value), schema.type)
|
||||
}
|
||||
} else {
|
||||
//先前已经有过赋值的列数据
|
||||
if (excludes.indexOf(schema.column) > -1) {
|
||||
continue
|
||||
}
|
||||
//如果是bool类型的变量,并且值为空不进行类型转换
|
||||
if (['boolean', 'bool'].indexOf(schema.format) > -1) {
|
||||
if (typeof raw[schema.column] === 'undefined') {
|
||||
continue
|
||||
}
|
||||
if (typeof raw[schema.column] === 'string' && raw[schema.column] == '') {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (raw[schema.column]) {
|
||||
data[schema.column] = mustMarshal(raw[schema.column], schema.type)
|
||||
} else {
|
||||
data[schema.column] = mustMarshal(
|
||||
scenario === 'create' ? schema.attribute.default_value : '',
|
||||
schema.type
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库数据解码为模型数据
|
||||
* @param {Object} raw
|
||||
* @param {Array} schemas
|
||||
* @returns
|
||||
*/
|
||||
export function decode(raw, schemas, scenario) {
|
||||
let data = {}
|
||||
scenario = scenario || 'unknown';
|
||||
if (!raw || !schemas) {
|
||||
return data
|
||||
}
|
||||
for (let schema of schemas) {
|
||||
if (
|
||||
schema.attribute.live.enable &&
|
||||
schema.attribute.live.type == 'cascader'
|
||||
) {
|
||||
let columns = schema.attribute.live.columns
|
||||
//级联数据如果没有配置字段,那么对结果进行JSON解码处理
|
||||
if (!Array.isArray(columns) || columns.length == 0) {
|
||||
if (raw[schema.column]) {
|
||||
data[schema.column] = JSON.parse(raw[schema.column])
|
||||
} else {
|
||||
data[schema.column] = []
|
||||
}
|
||||
} else {
|
||||
data[schema.column] = []
|
||||
for (let column of columns) {
|
||||
let v = raw[column]
|
||||
if (!v) {
|
||||
break
|
||||
}
|
||||
data[schema.column].push(mustMarshal(v, schema.type))
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(schema.attribute.live.enable &&
|
||||
schema.attribute.live.type == 'dropdown') ||
|
||||
schema.format == 'dropdown'
|
||||
) {
|
||||
//强制重新设置字段的format
|
||||
if (schema.format !== 'dropdown') {
|
||||
schema.format = 'dropdown';
|
||||
}
|
||||
//下拉列转成字符串显示
|
||||
let v = null;
|
||||
if (raw.hasOwnProperty(schema.column)) {
|
||||
v = raw[schema.column];
|
||||
} else if (scenario === 'create') {
|
||||
v = schema.attribute.default_value;
|
||||
}
|
||||
data[schema.column] = mustMarshal(v || '', 'string')
|
||||
} else if (schema.format === 'multiSelect') {
|
||||
try {
|
||||
if (typeof raw[schema.column] === 'string') {
|
||||
data[schema.column] = JSON.parse(raw[schema.column])
|
||||
}
|
||||
} catch (e) { }
|
||||
} else if (['time', 'date', 'datetime', 'timestamp'].indexOf(schema.format) > -1) {
|
||||
if (raw.hasOwnProperty(schema.column)) {
|
||||
let v = raw[schema.column];
|
||||
if (v) {
|
||||
if (typeof v === 'string' && v.indexOf('/') > -1) {
|
||||
let ss = v.split('/', 2);
|
||||
let st = toDate(ss[0]);
|
||||
let et = toDate(ss[1]);
|
||||
switch (schema.format) {
|
||||
case 'time':
|
||||
data[schema.column] = [dayjs(st).format('HH:mm'), dayjs(et).format('HH:mm')]
|
||||
break;
|
||||
case 'date':
|
||||
data[schema.column] = [dayjs(st).format('YYYY-MM-DD'), dayjs(et).format('YYYY-MM-DD')];
|
||||
break;
|
||||
default:
|
||||
data[schema.column] = [dayjs(st).format('YYYY-MM-DD HH:mm:ss'), dayjs(et).format('YYYY-MM-DD HH:mm:ss')];
|
||||
break
|
||||
}
|
||||
} else {
|
||||
let ts = toDate(v);
|
||||
switch (schema.format) {
|
||||
case 'time':
|
||||
data[schema.column] = dayjs(ts).format('HH:mm');
|
||||
break;
|
||||
case 'date':
|
||||
data[schema.column] = dayjs(ts).format('YYYY-MM-DD');
|
||||
break;
|
||||
default:
|
||||
data[schema.column] = dayjs(ts).format('YYYY-MM-DD HH:mm:ss');
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let v = null;
|
||||
if (raw.hasOwnProperty(schema.column)) {
|
||||
v = raw[schema.column];
|
||||
} else if (scenario === 'create') {
|
||||
v = schema.attribute.default_value;
|
||||
}
|
||||
data[schema.column] = mustMarshal(v || '', schema.type)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
|
@ -0,0 +1,680 @@
|
|||
import httpclicent from '@/apis/request'
|
||||
import pluralize from 'pluralize';
|
||||
import { getModelValue } from './model';
|
||||
|
||||
class CRUD {
|
||||
constructor(opts) {
|
||||
opts = Object.assign({
|
||||
moduleName: '',
|
||||
tableName: '',
|
||||
apiPrefix: '/v1',
|
||||
schemas: [],
|
||||
}, opts);
|
||||
this.opts = opts
|
||||
this.primaryKey = ''
|
||||
this.modelName = opts.moduleName
|
||||
this.tableName = opts.tableName
|
||||
this.models = []
|
||||
this.sortable = []
|
||||
this.queryParams = {}
|
||||
this.fixedQueueParams = {}
|
||||
this.pagination = {
|
||||
index: 1,
|
||||
size: 15,
|
||||
totalCount: 0,
|
||||
}
|
||||
//初始化schema
|
||||
if (Array.isArray(opts.schemas)) {
|
||||
this.schemas = opts.schemas
|
||||
} else if (!Array.isArray(opts.schemas) && typeof opts.Array === 'object') {
|
||||
this.schemas = [];
|
||||
for (let k in opts.schemas) {
|
||||
this.schemas.push(opts.schemas[k])
|
||||
}
|
||||
} else {
|
||||
this.schemas = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预处理一些变量
|
||||
*/
|
||||
__prepare() {
|
||||
for (let i in this.schemas) {
|
||||
let schema = this.schemas[i]
|
||||
this.schemas[i].error = ''
|
||||
if (schema.attribute.primary_key) {
|
||||
this.primaryKey = schema.column
|
||||
}
|
||||
if (schema.attribute.live.enable) {
|
||||
this._lazyFetach(schema).then(res => {
|
||||
schema.attribute.values = res
|
||||
}).catch(e => {
|
||||
console.log(e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lazyFetach(schema) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (schema.attribute.live.method === 'post') {
|
||||
httpclicent.post(
|
||||
schema.attribute.live.url,
|
||||
schema.attribute.live.body,
|
||||
{
|
||||
headers: { 'Content-Type': schema.attribute.live.content_type }
|
||||
}
|
||||
).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
} else {
|
||||
httpclicent.get(schema.attribute.live.url).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新列表的一个模型数据
|
||||
* @param {*} primaryKey
|
||||
*/
|
||||
__refreshModel(primaryKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let qs = {}
|
||||
qs[this.primaryKey] = primaryKey
|
||||
qs['__format'] = 'both'
|
||||
qs['scenario'] = 'list'
|
||||
this.getModel(qs).then(res => {
|
||||
let isExists = false;
|
||||
let primaryKey = this.findModelPrimaryKey(res);
|
||||
if (primaryKey === false) {
|
||||
reject('can not find model primary key')
|
||||
return
|
||||
}
|
||||
for (let k in this.models) {
|
||||
let pk = getModelValue(this.models[k], this.primaryKey);
|
||||
if (pk == primaryKey) {
|
||||
isExists = true
|
||||
this.models[k] = res
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isExists) {
|
||||
this.models.push(res)
|
||||
}
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个模型
|
||||
* @param {*} primaryKey
|
||||
*/
|
||||
__removeModel(primaryKey) {
|
||||
for (let k in this.models) {
|
||||
let pk = getModelValue(this.models[k], this.primaryKey);
|
||||
if (primaryKey === pk) {
|
||||
this.models.splice(k, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求的接口地址
|
||||
* @param {String} scenario
|
||||
* @param {any} primaryKey
|
||||
* @returns
|
||||
*/
|
||||
__buildUri(scenario, primaryKey) {
|
||||
return this.__buildModelUri(this.modelName, this.tableName, scenario, primaryKey);
|
||||
}
|
||||
|
||||
__buildModelUri(moduleName, tableNanme, scenario, primaryKey) {
|
||||
primaryKey = primaryKey || ''
|
||||
let uri = this.opts.apiPrefix ? this.opts.apiPrefix : '';
|
||||
let pluralizeName = pluralize.plural(tableNanme);
|
||||
let singularName = pluralize.singular(tableNanme);
|
||||
switch (scenario) {
|
||||
case 'create':
|
||||
uri += `/${moduleName}/${singularName}`
|
||||
break
|
||||
case 'update':
|
||||
uri += `/${moduleName}/${singularName}/${primaryKey}`
|
||||
break
|
||||
case 'delete':
|
||||
uri += `/${moduleName}/${singularName}/${primaryKey}`
|
||||
break
|
||||
case 'get':
|
||||
uri += `/${moduleName}/${singularName}/${primaryKey}`
|
||||
break
|
||||
case 'search':
|
||||
uri += `/${moduleName}/${pluralizeName}`
|
||||
break
|
||||
case 'export':
|
||||
uri += `/${moduleName}/${singularName}-export`
|
||||
break
|
||||
case 'import':
|
||||
uri += `/${moduleName}/${singularName}-import`
|
||||
break
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化CRUD组件
|
||||
* @returns
|
||||
*/
|
||||
initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (Array.isArray(this.schemas) && this.schemas.length > 0) {
|
||||
this.__prepare()
|
||||
resolve(this, this.schemas)
|
||||
return
|
||||
}
|
||||
httpclicent.get(`/rest/schema/${this.modelName}/${this.tableName}`).then(res => {
|
||||
this.schemas = res;
|
||||
this.__prepare()
|
||||
resolve(this, this.schemas);
|
||||
}).catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的字段描述信息
|
||||
* @returns
|
||||
*/
|
||||
getSchemas() {
|
||||
return this.schemas
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指定列的错误信息
|
||||
* @param {string} column
|
||||
* @param {string} error
|
||||
*/
|
||||
setColumnError(column, error) {
|
||||
for (let i in this.schemas) {
|
||||
if (this.schemas[i].column === column) {
|
||||
this.schemas[i].error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有列的错误信息
|
||||
*/
|
||||
resetError() {
|
||||
for (let i in this.schemas) {
|
||||
this.schemas[i].error = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模型集合
|
||||
* @returns
|
||||
*/
|
||||
getModels() {
|
||||
return this.models;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分页索引
|
||||
* @param {Number} index 分页索引
|
||||
* @returns CRUD
|
||||
*/
|
||||
setPaginationIndex(index) {
|
||||
this.pagination.index = index
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前分页的索引
|
||||
* @returns
|
||||
*/
|
||||
getPaginationIndex() {
|
||||
return this.pagination.index
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分页大小
|
||||
* @returns CRUD
|
||||
* @param {Number} pageSize 分页大小
|
||||
*/
|
||||
setPaginationSize(pageSize) {
|
||||
this.pagination.size = pageSize
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页大小
|
||||
* @returns
|
||||
*/
|
||||
getPaginationSize() {
|
||||
return this.pagination.size || 10
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取分页信息中的总数据条数
|
||||
* @returns
|
||||
*/
|
||||
getPaginationCount() {
|
||||
return this.pagination.totalCount
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置分页配置
|
||||
* @returns CRUD
|
||||
*/
|
||||
resetPagination() {
|
||||
this.pagination.index = 1
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字段排序
|
||||
* @param {String} column 字段列
|
||||
* @param {String} order 排序规则
|
||||
* @returns CRUD
|
||||
*/
|
||||
setSortable(column, order) {
|
||||
if (!column) {
|
||||
this.sortable = {}
|
||||
} else {
|
||||
this.sortable = {
|
||||
column: column,
|
||||
order: order || 'ascending',
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加固定的查询参数
|
||||
* @param {*} k
|
||||
* @param {*} v
|
||||
*/
|
||||
addFixedQueueParams(k, v) {
|
||||
this.fixedQueueParams[k] = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查询参数
|
||||
* @param {Object} qs 查询参数
|
||||
* @returns
|
||||
*/
|
||||
setQueryParams(qs) {
|
||||
this.queryParams = qs
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加查询参数
|
||||
* @param {String} k
|
||||
* @param {String} v
|
||||
*/
|
||||
addQueryParams(k, v) {
|
||||
this.queryParams[k] = v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找模型的主键的值
|
||||
* @param {Object} model 表模型
|
||||
* @returns Boolean | mixed
|
||||
*/
|
||||
findModelPrimaryKey(model) {
|
||||
if (typeof model === 'object') {
|
||||
return getModelValue(model, this.primaryKey);
|
||||
} else {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个模型
|
||||
* @param {Object} model
|
||||
*/
|
||||
createModel(model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.post(this.__buildUri('create'), model).then(res => {
|
||||
this.__refreshModel(res.id).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建一个资源
|
||||
* @param {String} moduleName
|
||||
* @param {String} tableName
|
||||
* @param {Object} model
|
||||
* @returns
|
||||
*/
|
||||
createResource(moduleName, tableName, model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.post(this.__buildModelUri(moduleName, tableName, 'create'), model).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新一个模型
|
||||
* @param {Object} model
|
||||
*/
|
||||
updateModel(model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let primaryKey = this.findModelPrimaryKey(model);
|
||||
if (primaryKey === false) {
|
||||
reject('can not find model primary key')
|
||||
return
|
||||
}
|
||||
httpclicent.put(this.__buildUri('update', primaryKey), model).then(res => {
|
||||
this.__refreshModel(primaryKey).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新一个资源
|
||||
* @param {String} moduleName
|
||||
* @param {String} tableName
|
||||
* @param {Object} model
|
||||
*/
|
||||
updateResource(moduleName, tableName, model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let primaryKey = this.findModelPrimaryKey(model);
|
||||
if (primaryKey === false) {
|
||||
reject('can not find model primary key')
|
||||
return
|
||||
}
|
||||
httpclicent.put(this.__buildModelUri(moduleName, tableName, 'update', primaryKey), model).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个模型
|
||||
* @param {Object} model
|
||||
*/
|
||||
deleteModel(model) {
|
||||
let primaryKey = '';
|
||||
if (typeof model === 'object') {
|
||||
primaryKey = this.findModelPrimaryKey(model)
|
||||
} else {
|
||||
primaryKey = model
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.delete(this.__buildUri('delete', primaryKey), model).then(res => {
|
||||
this.__removeModel(primaryKey)
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个资源
|
||||
* @param {String} moduleName
|
||||
* @param {String} tableName
|
||||
* @param {Object} model
|
||||
* @returns
|
||||
*/
|
||||
deleteResource(moduleName, tableName, model) {
|
||||
let primaryKey = '';
|
||||
if (typeof model === 'object') {
|
||||
primaryKey = this.findModelPrimaryKey(model)
|
||||
} else {
|
||||
primaryKey = model
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.delete(this.__buildModelUri(moduleName, tableName, 'delete', primaryKey), model).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取一个资源
|
||||
* @param {String} moduleName
|
||||
* @param {String} tableName
|
||||
* @param {Object} model
|
||||
* @returns
|
||||
*/
|
||||
getResource(moduleName, tableName, qs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let primaryKey = '';
|
||||
let params = {}
|
||||
if (typeof qs === 'object') {
|
||||
for (let k in qs) {
|
||||
if (k === this.primaryKey) {
|
||||
primaryKey = qs[k]
|
||||
} else {
|
||||
params[k] = qs[k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
primaryKey = qs
|
||||
}
|
||||
if (!primaryKey) {
|
||||
reject('can not find model primary key')
|
||||
return
|
||||
}
|
||||
httpclicent.get(this.__buildModelUri(moduleName, tableName, 'get', primaryKey), { params: params }).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} pk
|
||||
* @param {*} qs
|
||||
* @returns
|
||||
*/
|
||||
fetchModel(pk, qs) {
|
||||
if (typeof qs !== 'object') {
|
||||
qs = {}
|
||||
}
|
||||
qs[this.primaryKey] = pk
|
||||
return this.getModel(qs)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个模型数据
|
||||
* @param {*} qs
|
||||
*/
|
||||
getModel(qs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let primaryKey = '';
|
||||
let params = {}
|
||||
if (typeof qs === 'object') {
|
||||
for (let k in qs) {
|
||||
if (k === this.primaryKey) {
|
||||
primaryKey = qs[k]
|
||||
} else {
|
||||
params[k] = qs[k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
primaryKey = qs
|
||||
}
|
||||
if (!primaryKey) {
|
||||
reject('can not find model primary key')
|
||||
return
|
||||
}
|
||||
httpclicent.get(this.__buildUri('get', primaryKey), { params: params }).then(res => {
|
||||
resolve(res)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询模型数据
|
||||
*/
|
||||
searchModel() {
|
||||
let queryParams = Object.assign({}, this.queryParams);
|
||||
queryParams['page'] = this.pagination.index
|
||||
queryParams['pagesize'] = this.pagination.size || 15
|
||||
if (this.sortable && this.sortable.column != '') {
|
||||
queryParams['sort'] = this.sortable.order == 'descending' ? '-' + this.sortable.column : this.sortable.column
|
||||
}
|
||||
queryParams['__format'] = 'both';
|
||||
if (typeof this.fixedQueueParams === 'object') {
|
||||
for (let k in this.fixedQueueParams) {
|
||||
queryParams[k] = this.fixedQueueParams[k];
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.get(this.__buildUri('search'), { params: queryParams }).then(res => {
|
||||
this.pagination.index = parseInt(res.page);
|
||||
this.pagination.size = parseInt(res.pagesize);
|
||||
this.pagination.totalCount = parseInt(res.totalCount);
|
||||
this.models = res.data
|
||||
resolve()
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除模型
|
||||
* @param {Array} data
|
||||
*/
|
||||
deleteModels(data) {
|
||||
let pks = [];
|
||||
for (let val of data) {
|
||||
if (typeof val === 'object') {
|
||||
let pk = this.findModelPrimaryKey(val)
|
||||
if (pk) {
|
||||
pks.push(pk)
|
||||
}
|
||||
} else {
|
||||
pks.push(val)
|
||||
}
|
||||
}
|
||||
if (pks.length <= 0) {
|
||||
return
|
||||
}
|
||||
let ps = [];
|
||||
for (let pk of pks) {
|
||||
ps.push(this.deleteModel(pk))
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.all(ps).then(res => {
|
||||
let data = {
|
||||
total: 0,
|
||||
success: 0,
|
||||
responses: res,
|
||||
};
|
||||
data.total = pks.length
|
||||
data.success = res.length
|
||||
resolve(data)
|
||||
}).catch(e => {
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载指定资源的导入模板
|
||||
* @param {String} moduleName
|
||||
* @param {String} tableName
|
||||
* @returns
|
||||
*/
|
||||
downloadResourceTemplate(moduleName, tableName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.get(this.__buildModelUri(moduleName, tableName, 'import')).then(res => {
|
||||
let element = document.createElement('a')
|
||||
let disposition = res.headers['content-disposition']
|
||||
let filename = this.tableName + '.csv';
|
||||
//获取文件名称
|
||||
if (disposition) {
|
||||
let pos = -1;
|
||||
let ps = disposition.split(';')
|
||||
for (let s of ps) {
|
||||
pos = s.indexOf('filename=')
|
||||
if (pos === 0) {
|
||||
filename = s.substring(9)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
element.style.display = 'none'
|
||||
element.href = window.URL.createObjectURL(new Blob([res.data], { type: res.headers['content-type'] }))
|
||||
element.target = '_blank'
|
||||
element.setAttribute('download', filename)
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
window.URL.revokeObjectURL(element.href)
|
||||
document.body.removeChild(element)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载导入模板
|
||||
* @returns
|
||||
*/
|
||||
downloadTemplate() {
|
||||
return this.downloadResourceTemplate(this.modelName, this.tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出模型数据
|
||||
* @returns
|
||||
*/
|
||||
exportModels() {
|
||||
let queryParams = Object.assign({}, this.queryParams);
|
||||
if (this.sortable && this.sortable.column != '') {
|
||||
queryParams['sort'] = this.sortable.order == 'descending' ? '-' + this.sortable.column : this.sortable.column
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpclicent.get(this.__buildUri('export'), { params: queryParams }).then(res => {
|
||||
let element = document.createElement('a')
|
||||
let disposition = res.headers['content-disposition']
|
||||
let filename = this.tableName + '.csv';
|
||||
//获取文件名称
|
||||
if (disposition) {
|
||||
let pos = -1;
|
||||
let ps = disposition.split(';')
|
||||
for (let s of ps) {
|
||||
pos = s.indexOf('filename=')
|
||||
if (pos === 0) {
|
||||
filename = s.substring(9)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
element.style.display = 'none'
|
||||
element.href = window.URL.createObjectURL(new Blob([res.data], { type: res.headers['content-type'] }))
|
||||
element.target = '_blank'
|
||||
element.setAttribute('download', filename)
|
||||
document.body.appendChild(element)
|
||||
element.click()
|
||||
window.URL.revokeObjectURL(element.href)
|
||||
document.body.removeChild(element)
|
||||
}).catch(e => { reject(e) })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default CRUD
|
|
@ -0,0 +1,9 @@
|
|||
class ValidateError extends Error {
|
||||
constructor(column) {
|
||||
super(column.message)
|
||||
this.schema = column
|
||||
this.name = 'ValidateError'
|
||||
}
|
||||
}
|
||||
|
||||
export default ValidateError
|
|
@ -0,0 +1,161 @@
|
|||
|
||||
/**
|
||||
* 清理搜索模型的空字段
|
||||
* @param {Object} model
|
||||
* @param {Array} schemas
|
||||
*/
|
||||
export function clearSearchModel(model, schemas) {
|
||||
let ps = {}
|
||||
for (let schema of schemas) {
|
||||
if (schema.type === 'string') {
|
||||
if (model[schema.column] === '') {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (['integer', 'double'].indexOf(schema.type) > -1 && ['boolean', 'bool'].indexOf(schema.format) === -1) {
|
||||
if (model[schema.column] === 0) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
ps[schema.column] = model[schema.column]
|
||||
}
|
||||
return ps;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断指定的字段是否显示
|
||||
* @param {Object} schema
|
||||
* @param {Object} model
|
||||
*/
|
||||
export function checkSchemaVisible(schema, model) {
|
||||
if (!Array.isArray(schema.attribute.visible)) {
|
||||
return true
|
||||
}
|
||||
if (schema.attribute.visible.length <= 0) {
|
||||
return true
|
||||
}
|
||||
for (let cond of schema.attribute.visible) {
|
||||
let column = cond['column']
|
||||
let values = cond['values']
|
||||
if (!column || column === '') {
|
||||
continue
|
||||
}
|
||||
values.map((v) => {
|
||||
return v + ''
|
||||
})
|
||||
let compareValue = model[column]
|
||||
if (typeof compareValue === 'undefined') {
|
||||
return false
|
||||
}
|
||||
if (values.indexOf(compareValue + '') === -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成规则
|
||||
* @param {Object} schema
|
||||
* @param {String} scenario
|
||||
* @returns
|
||||
*/
|
||||
export function generateSchemaRule(schema, scenario) {
|
||||
let rules = []
|
||||
if (schema.rule.required.indexOf(scenario) > -1) {
|
||||
rules.push({
|
||||
required: true,
|
||||
message: `${schema.label}字段的值不能为空`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
}
|
||||
//长度过滤把在线搜索的情况过滤掉
|
||||
if (['string', 'text'].indexOf(schema.type) > -1 && schema.format !== 'cascader') {
|
||||
rules.push({
|
||||
type: 'string',
|
||||
message: `${schema.label}字段的值不是有效的字符串`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
if (schema.rule.max > 0 && schema.rule.min > 0) {
|
||||
rules.push({
|
||||
max: schema.rule.max,
|
||||
min: schema.rule.min,
|
||||
message: `${schema.label}字段长度必须在${schema.rule.min} - ${schema.rule.max}个字符之间`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
} else if (schema.rule.max > 0) {
|
||||
rules.push({
|
||||
max: schema.rule.max,
|
||||
message: `${schema.label}字段长度不能大于${schema.rule.max}个字符`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if (['integer', 'decimal', 'number'].indexOf(schema.type) > -1) {
|
||||
if (['date', 'datetime', 'timestamp'].indexOf(schema.format) == -1) {
|
||||
rules.push({
|
||||
type: 'number',
|
||||
message: `${schema.label}字段的值不是有效的数字`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
if (schema.rule.min !== 0 && schema.rule.max !== 0) {
|
||||
rules.push({
|
||||
max: schema.rule.max,
|
||||
min: schema.rule.min,
|
||||
message: `${schema.label}的值必须在${schema.rule.min} - ${schema.rule.max}之间`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
} else if (schema.rule.max !== 0) {
|
||||
// rules.push({
|
||||
// max: schema.rule.max,
|
||||
// message: `${schema.label}最大值不能大于${schema.rule.max}`,
|
||||
// trigger: 'blur',
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (['date', 'datetime', 'timestamp'].indexOf(schema.format) > -1) {
|
||||
rules.push({
|
||||
type: 'date',
|
||||
message: `${schema.label}字段的值是无效的`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
}
|
||||
if (schema.rule.regular !== '') {
|
||||
rules.push({
|
||||
type: 'regexp',
|
||||
pattern: schema.rule.regular,
|
||||
message: `${schema.label}字段的值不符合规范`,
|
||||
trigger: 'blur',
|
||||
})
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
export function generateSchemaDescription(schema, scenario) {
|
||||
if (typeof schema.attribute.description === 'string' && schema.attribute.description !== '') {
|
||||
return schema.attribute.description
|
||||
}
|
||||
let messages = []
|
||||
if (schema.rule.required.indexOf(scenario) > -1) {
|
||||
messages.push("不能为空")
|
||||
}
|
||||
if (['string', 'text'].indexOf(schema.type) > -1) {
|
||||
if (schema.rule.max > 0 && schema.rule.min > 0) {
|
||||
messages.push(`长度必须在${schema.rule.min} - ${schema.rule.max}个字符之间`)
|
||||
} else if (schema.rule.max > 0) {
|
||||
messages.push(`长度必须小于${schema.rule.max}个字符`)
|
||||
}
|
||||
}
|
||||
if (['integer', 'decimal', 'number'].indexOf(schema.type) > -1) {
|
||||
if (schema.rule.max > 0 && schema.rule.min > 0) {
|
||||
messages.push(`值必须在${schema.rule.min} - ${schema.rule.max}之间`)
|
||||
} else if (schema.rule.max > 0) {
|
||||
messages.push(`值必须小于${schema.rule.max}`)
|
||||
}
|
||||
}
|
||||
return messages.join(', ')
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
export function getModelValue(model, column) {
|
||||
if (typeof model !== 'object') {
|
||||
return null;
|
||||
}
|
||||
if (!model.hasOwnProperty(column)) {
|
||||
return null;
|
||||
}
|
||||
let value = model[column];
|
||||
if (typeof value === 'object' && value.hasOwnProperty('value')) {
|
||||
return value['value']
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
export function getModelLabel(model, column) {
|
||||
if (typeof model !== 'object') {
|
||||
return null;
|
||||
}
|
||||
if (!model.hasOwnProperty(column)) {
|
||||
return null;
|
||||
}
|
||||
let value = model[column];
|
||||
if (typeof value === 'object' && value.hasOwnProperty('label')) {
|
||||
return value['label']
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function modelValueOf(model) {
|
||||
if (typeof model !== 'object') {
|
||||
return {};
|
||||
}
|
||||
let result = {}
|
||||
for (let i in model) {
|
||||
let row = model[i];
|
||||
if (typeof row === 'object' && row.hasOwnProperty('value')) {
|
||||
result[i] = row['value']
|
||||
} else {
|
||||
result[i] = row
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function modelLabelOf(model) {
|
||||
if (typeof model !== 'object') {
|
||||
return {};
|
||||
}
|
||||
let result = {}
|
||||
for (let i in model) {
|
||||
let row = model[i];
|
||||
if (typeof row === 'object' && row.hasOwnProperty('label')) {
|
||||
result[i] = row['label']
|
||||
} else {
|
||||
result[i] = row
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<template v-if="action.icon">
|
||||
<template v-if="action.label">
|
||||
<el-tooltip effect="dark" :content="action.label" placement="top-start">
|
||||
<icon @click="handleSubmit" :name="action.icon" class="segment-action cursor-pointer"
|
||||
:class="`text-color-${action.type || 'default'}`">
|
||||
</icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon @click="handleSubmit" :name="action.icon" class="segment-action cursor-pointer"
|
||||
:class="`text-color-${action.type || 'default'}`"></icon>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button :type="action.type || 'default'" :round="action.round || false" :size="action.size" class="segment-action" @click="handleSubmit">
|
||||
{{ action.label }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.segment-action {
|
||||
margin-right: .5rem;
|
||||
|
||||
&:last-child{
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/components/widgets/Icon.vue';
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const handleSubmit = () => {
|
||||
emit('click', props.action)
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<template v-if="tagVisible">
|
||||
<span class="segment-gridview-cell segment-tag" :style="{ color: textColor, backgroundColor: backgroundColor }">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="['boolean', 'bool'].indexOf(schema.format) > -1">
|
||||
<el-tag round :type="isTrue ? 'success' : 'danger'"> {{ formatBoolean(isTrue) }}</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="segment-gridview-cell">{{ value }}</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Color from 'color'
|
||||
import { getModelValue } from '../libs/model';
|
||||
|
||||
const props = defineProps({
|
||||
model: {
|
||||
type: Object,
|
||||
},
|
||||
schema: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const recursiveFind = (hack, values) => {
|
||||
if (!Array.isArray(values)) {
|
||||
return hack;
|
||||
}
|
||||
for (let i in values) {
|
||||
let row = values[i];
|
||||
if (row.value === hack) {
|
||||
return row.label;
|
||||
}
|
||||
if (row.children && Array.isArray(row.children)) {
|
||||
let v = recursiveFind(hack, row.children)
|
||||
if (v) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const valueFind = (hack, values) => {
|
||||
let result = '';
|
||||
let item = '';
|
||||
if (!Array.isArray(values)) {
|
||||
return hack;
|
||||
}
|
||||
if (Array.isArray(hack)) {
|
||||
item = hack.shift()
|
||||
} else {
|
||||
item = hack;
|
||||
}
|
||||
for (let i in values) {
|
||||
let row = values[i];
|
||||
if (row.value === item) {
|
||||
result = row.label;
|
||||
if (row.children && Array.isArray(row.children)) {
|
||||
let childVal = valueFind(hack, row.children);
|
||||
if (childVal) {
|
||||
result += "/" + childVal;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回渲染的值
|
||||
*/
|
||||
const value = computed(() => {
|
||||
let model = '';
|
||||
let text = '';
|
||||
let value = '';
|
||||
let str = '';
|
||||
if (props.model[props.schema.column]) {
|
||||
model = props.model[props.schema.column]
|
||||
}
|
||||
if (typeof model === 'object') {
|
||||
text = model.label;
|
||||
value = model.value;
|
||||
} else {
|
||||
text = model
|
||||
value = model
|
||||
}
|
||||
if (props.schema.format === 'cascader' && Array.isArray(props.schema.attribute.values) && props.schema.attribute.values.length > 0) {
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
str = valueFind(value, props.schema.attribute.values)
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
str = valueFind(value, props.schema.attribute.values)
|
||||
} else {
|
||||
str = value;
|
||||
}
|
||||
} else {
|
||||
str = recursiveFind(value, props.schema.attribute.values);
|
||||
}
|
||||
if (str) {
|
||||
text = str;
|
||||
}
|
||||
return text;
|
||||
})
|
||||
|
||||
const isTrue = computed(() => {
|
||||
let value = getModelValue(props.model, props.schema.column);
|
||||
if (typeof value === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return value ? true : false
|
||||
})
|
||||
|
||||
const formatBoolean = (v) => {
|
||||
if (v) {
|
||||
return '是'
|
||||
} else {
|
||||
return '否'
|
||||
}
|
||||
}
|
||||
|
||||
const tagVisible = computed(() => {
|
||||
if (['dropdown'].indexOf(props.schema.format) > -1) {
|
||||
let hasColor = true
|
||||
//如果没有颜色渲染,那么渲染普通样式
|
||||
for (let i in props.schema.attribute.values) {
|
||||
if (!props.schema.attribute.values[i].color) {
|
||||
hasColor = false
|
||||
}
|
||||
}
|
||||
return hasColor
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const textColor = computed(() => {
|
||||
if (['dropdown'].indexOf(props.schema.format) > -1) {
|
||||
let data = props.model[props.schema.column];
|
||||
let values = props.schema.attribute.values;
|
||||
for (let row of values) {
|
||||
if (typeof data === 'object') {
|
||||
if (row.value == data.value) {
|
||||
if (row.color) {
|
||||
return row.color
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (row.label == data || row.value == data) {
|
||||
if (row.color) {
|
||||
return row.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
|
||||
const backgroundColor = computed(() => {
|
||||
if (['dropdown'].indexOf(props.schema.format) > -1) {
|
||||
let data = props.model[props.schema.column];
|
||||
let values = props.schema.attribute.values;
|
||||
for (let row of values) {
|
||||
if (typeof data === 'object') {
|
||||
if (row.value == data.value) {
|
||||
if (row.color) {
|
||||
return Color(row.color).fade(0.9).hsl()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (row.label == data || row.value == data) {
|
||||
if (row.color) {
|
||||
return Color(row.color).fade(0.9).hsl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
|
||||
</script>
|
|
@ -0,0 +1,281 @@
|
|||
<template>
|
||||
<template v-if="isVisible('time', schema)">
|
||||
<el-time-select v-model="model[schema.column]" start="00:00" step="00:15" end="23:59"
|
||||
:disabled="isDisabled(schema)" :placeholder="getPlaceholder(schema)" format="HH:mm" />
|
||||
</template>
|
||||
<template v-else-if="isVisible('date', schema)">
|
||||
<el-date-picker v-if="isRange(schema)" :disabled="isDisabled(schema)" type="daterange" :editable="false"
|
||||
format="YYYY-MM-DD" value-format="YYYY-MM-DD" v-model="model[schema.column]"
|
||||
:start-placeholder="getStartPlaceholder(schema)"
|
||||
:end-placeholder="getEndPlaceholder(schema)"></el-date-picker>
|
||||
<el-date-picker v-else :disabled="isDisabled(schema)" type="date" v-model="model[schema.column]"
|
||||
:editable="false" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
|
||||
:placeholder="getPlaceholder(schema)"></el-date-picker>
|
||||
</template>
|
||||
<template v-else-if="isVisible('datetime', schema)">
|
||||
<el-date-picker v-if="isRange(schema)" :disabled="isDisabled(schema)" :editable="false" format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" time-format="HH:mm:ss" type="datetimerange" :prefix-icon="Calendar"
|
||||
v-model="model[schema.column]" :teleported="false" :start-placeholder="getStartPlaceholder(schema)"
|
||||
:disabled-date="getIsDateDisable" :end-placeholder="getEndPlaceholder(schema)"></el-date-picker>
|
||||
<el-date-picker v-else :disabled="isDisabled(schema)" type="datetime" :editable="false"
|
||||
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" v-model="model[schema.column]"
|
||||
:placeholder="getPlaceholder(schema)"></el-date-picker>
|
||||
</template>
|
||||
<template v-else-if="isVisible('dropdown', schema)">
|
||||
<el-select clearable v-if="!isDropdownMulitLevel(schema)" v-model="model[schema.column]"
|
||||
:multiple="schema.format === 'multiSelect'" :disabled="isDisabled(schema)"
|
||||
:placeholder="getPlaceholder(schema)" :allow-create="isDropdownCreated(schema)"
|
||||
:filterable="isDropdownFilterable(schema)" :default-first-option="isDropdownDefaultFirst(schema)">
|
||||
<template v-for="(v, k) in schema.attribute.values" :key="k">
|
||||
<el-option v-if="typeof v === 'object'" :label="v.label" :value="v.value"></el-option>
|
||||
<el-option v-else :label="v" :value="k"></el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
<level-select v-model="model[schema.column]" :options="schema.attribute.values" clearable v-else></level-select>
|
||||
</template>
|
||||
<template v-else-if="isVisible('search_boolean', schema)">
|
||||
<el-select clearable v-model="model[schema.column]">
|
||||
<template v-for="(v, k) in booleanValues" :key="k">
|
||||
<el-option :label="v" :value="k"></el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else-if="isVisible('cascader', schema)">
|
||||
<el-cascader :disabled="isDisabled(schema)" :options="schema.attribute.values" filterable clearable
|
||||
:placeholder="getPlaceholder(schema)" v-model="model[schema.column]" :validate-event="false"></el-cascader>
|
||||
</template>
|
||||
<template v-else-if="isVisible('color', schema)">
|
||||
<el-color-picker v-model="model[schema.column]" :disabled="isDisabled(schema)" />
|
||||
</template>
|
||||
<template v-else-if="isVisible('boolean', schema)">
|
||||
<el-switch :disabled="isDisabled(schema)" :inactive-value="0" :active-value="1"
|
||||
v-model="model[schema.column]"></el-switch>
|
||||
</template>
|
||||
<template v-else-if="isVisible('file', schema)">
|
||||
<upload :action="schema.attribute.upload_url" v-model="model[schema.column]"></upload>
|
||||
</template>
|
||||
<template v-else-if="isVisible('password', schema)">
|
||||
<el-input :disabled="isDisabled(schema)" show-password :placeholder="getPlaceholder(schema)"
|
||||
v-model.trim="model[schema.column]"></el-input>
|
||||
</template>
|
||||
<template v-else-if="isVisible('multistr', schema)">
|
||||
<el-input type="textarea" :disabled="isDisabled(schema)" :placeholder="getPlaceholder(schema)"
|
||||
:show-word-limit="isShowWordLimit(schema)" :maxlength="schema.rule.max"
|
||||
v-model="model[schema.column]"></el-input>
|
||||
</template>
|
||||
<template v-else-if="isVisible('number', schema)">
|
||||
<el-input :disabled="isDisabled(schema)" :prefix-icon="schema.attribute.icon || ''"
|
||||
:placeholder="getPlaceholder(schema)" v-model.number="model[schema.column]"
|
||||
:clearable="isSearch ? true : false">
|
||||
<template v-if="schema.attribute.suffix" #append>
|
||||
{{ schema.attribute.suffix }}
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input :disabled="isDisabled(schema)" :prefix-icon="schema.attribute.icon || ''"
|
||||
:placeholder="getPlaceholder(schema)" v-model.trim="model[schema.column]"
|
||||
:clearable="isSearch ? true : false">
|
||||
<template v-if="schema.attribute.suffix" #append>
|
||||
{{ schema.attribute.suffix }}
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue';
|
||||
import Upload from './Upload.vue';
|
||||
import { Calendar } from '@element-plus/icons-vue'
|
||||
import { generateSchemaDescription } from '../libs/form';
|
||||
import LevelSelect from './LevelSelect.vue';
|
||||
const props = defineProps({
|
||||
model: {
|
||||
type: Object,
|
||||
},
|
||||
schema: {
|
||||
type: Object,
|
||||
},
|
||||
scenario: {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
|
||||
const isDropdownCreated = (schema) => {
|
||||
if (props.scenario === 'search') {
|
||||
return false;
|
||||
}
|
||||
let ret = schema.attribute.dropdown && schema.attribute.dropdown.created || false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
const isDropdownFilterable = (schema) => {
|
||||
if (props.scenario === 'search') {
|
||||
return false;
|
||||
}
|
||||
return schema.attribute.dropdown && schema.attribute.dropdown.filterable || false;
|
||||
}
|
||||
|
||||
const isDropdownDefaultFirst = (schema) => {
|
||||
return schema.attribute.dropdown && schema.attribute.dropdown.default_first || false;
|
||||
}
|
||||
|
||||
const isDropdownMulitLevel = (schema) => {
|
||||
let values = schema.attribute.values;
|
||||
if (Array.isArray(values)) {
|
||||
for (let i in values) {
|
||||
let row = values[i];
|
||||
if (row.children && Array.isArray(row.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const isShowWordLimit = (schema) => {
|
||||
return false
|
||||
}
|
||||
|
||||
const isDisabled = (schema) => {
|
||||
return schema.attribute.disable.indexOf(props.scenario) > -1 || schema.attribute.readonly.indexOf(props.scenario) > -1
|
||||
}
|
||||
|
||||
const booleanValues = computed(() => {
|
||||
return {
|
||||
1: '是',
|
||||
0: '否'
|
||||
}
|
||||
})
|
||||
|
||||
const maxLength = (schema) => {
|
||||
if (schema.type === 'string') {
|
||||
return schema.rule.max || 100
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
const isSearch = computed(() => {
|
||||
return props.scenario === 'search';
|
||||
})
|
||||
|
||||
|
||||
const getIsDateDisable = (date) => {
|
||||
if (props.schema.attribute.end_of_now) {
|
||||
return date.getTime() > (new Date()).getTime();
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isVisible = (type, schema) => {
|
||||
let visible = false
|
||||
switch (type) {
|
||||
case 'number':
|
||||
visible = (['integer', 'decimal'].indexOf(schema.format) > -1 || ['integer', 'double'].indexOf(schema.type) > -1)
|
||||
break
|
||||
case 'password':
|
||||
visible = ['password', 'pass'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'time':
|
||||
visible = ['time'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'date':
|
||||
visible = ['date'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'datetime':
|
||||
visible = ['datetime', 'timestamp'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'dropdown':
|
||||
visible = ['dropdown', 'multiSelect'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'cascader':
|
||||
visible = ['cascader'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'multistr':
|
||||
if (props.scenario !== 'search') {
|
||||
visible = ['textarea'].indexOf(schema.format) > -1
|
||||
}
|
||||
break
|
||||
case 'color':
|
||||
visible = ['color'].indexOf(schema.format) > -1
|
||||
break
|
||||
case 'file':
|
||||
if (props.scenario !== 'file') {
|
||||
if (['file'].indexOf(schema.format) > -1) {
|
||||
if (schema.attribute.upload_url) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
if (
|
||||
props.scenario !== 'search' &&
|
||||
['bool', 'boolean'].indexOf(schema.format) > -1
|
||||
) {
|
||||
visible = true
|
||||
} else {
|
||||
visible = false
|
||||
}
|
||||
break
|
||||
case 'search_boolean':
|
||||
if (
|
||||
props.scenario === 'search' &&
|
||||
['bool', 'boolean'].indexOf(schema.format) > -1
|
||||
) {
|
||||
visible = true
|
||||
} else {
|
||||
visible = false
|
||||
}
|
||||
break
|
||||
case 'string':
|
||||
visible = ['string', 'text'].indexOf(schema.format) > -1
|
||||
break
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
const getPlaceholder = (schema) => {
|
||||
let placeholder = '';
|
||||
if (schema.attribute.tooltip) {
|
||||
placeholder = schema.attribute.tooltip
|
||||
} else {
|
||||
if (['dropdown'].indexOf(schema.format) > -1) {
|
||||
placeholder = '请选择' + schema.label
|
||||
} else {
|
||||
placeholder = '请输入' + schema.label
|
||||
}
|
||||
}
|
||||
return placeholder
|
||||
}
|
||||
|
||||
const getStartPlaceholder = (schema) => {
|
||||
let placeholder = '';
|
||||
if (schema.attribute.tooltip) {
|
||||
placeholder = schema.attribute.tooltip
|
||||
} else {
|
||||
placeholder = '起始' + schema.label
|
||||
}
|
||||
return placeholder
|
||||
}
|
||||
|
||||
const getEndPlaceholder = (schema) => {
|
||||
let placeholder = '';
|
||||
if (schema.attribute.tooltip) {
|
||||
placeholder = schema.attribute.tooltip
|
||||
} else {
|
||||
placeholder = '结束' + schema.label
|
||||
}
|
||||
return placeholder
|
||||
}
|
||||
|
||||
const isRange = (schema) => {
|
||||
return props.scenario === 'search'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.schema.attribute.description = generateSchemaDescription(props.schema, props.scenario)
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<el-cascader v-model="value" :props="attributes" :options="options" :show-all-levels="false" @change="handleChange"
|
||||
:clearable="clearable" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const value = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const attributes = ref({
|
||||
multiple: false
|
||||
})
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (!Array.isArray(e)) {
|
||||
emit('update:modelValue', '');
|
||||
return
|
||||
}
|
||||
if (e.length > 1) {
|
||||
emit('update:modelValue', e[e.length - 1]);
|
||||
} else {
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
}
|
||||
|
||||
const hasChildren = (v, vs) => {
|
||||
if (!Array.isArray(vs)) {
|
||||
return false;
|
||||
}
|
||||
for (let i in vs) {
|
||||
if (vs[i].value == v) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue != '') {
|
||||
for (let i in props.options) {
|
||||
let row = props.options[i];
|
||||
if (hasChildren(props.modelValue, row.children)) {
|
||||
value.value = [row.value, props.modelValue];
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<template v-if="isAllowed">
|
||||
<router-link :to="to">
|
||||
<slot name="default"></slot>
|
||||
</router-link>
|
||||
</template>
|
||||
<span v-else>
|
||||
<slot name="default"></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import useUserStore from '@/stores/user';
|
||||
|
||||
const props = defineProps({
|
||||
permission: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const isAllowed = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.permission) {
|
||||
isAllowed.value = true;
|
||||
} else {
|
||||
let ps = route.name + '.' + props.permission
|
||||
const { hasPermission } = useUserStore();
|
||||
isAllowed.value = hasPermission(ps);
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<el-upload drag :action="uploadUrl" :auto-upload="true" :limit="1" :headers="uploadHeaders" :show-file-list="false"
|
||||
:on-success="onFileUploaded">
|
||||
<span v-if="!isUploaded">{{ label }}</span>
|
||||
<span v-else>
|
||||
已上传
|
||||
</span>
|
||||
</el-upload>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getBaseUrl } from '@/apis/request'
|
||||
import useUserStore from '@/stores/user'
|
||||
const userStore = useUserStore();
|
||||
|
||||
const props = defineProps({
|
||||
action: {
|
||||
type: String,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: "上传文件"
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const isUploaded = ref(false);
|
||||
|
||||
const uploadUrl = computed(() => {
|
||||
let uri = props.action;
|
||||
if (uri.indexOf('http') > -1) {
|
||||
return uri;
|
||||
} else {
|
||||
return getBaseUrl() + uri;
|
||||
}
|
||||
})
|
||||
|
||||
const uploadHeaders = computed(() => {
|
||||
return {
|
||||
Authorization: userStore.getAccessToken(),
|
||||
}
|
||||
})
|
||||
|
||||
const onFileUploaded = (e) => {
|
||||
console.log('upload response', e)
|
||||
if (e.hasOwnProperty('code')) {
|
||||
if (e.code === 0) {
|
||||
isUploaded.value = true;
|
||||
emit('update:modelValue', e.data.id);
|
||||
}
|
||||
}
|
||||
if (e.hasOwnProperty('errno')) {
|
||||
if (e.errno === 0) {
|
||||
isUploaded.value = true;
|
||||
emit('update:modelValue', e.result.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="segment-action" v-if="isAllowed">
|
||||
<el-text :type="type">
|
||||
<el-tooltip v-if="title != ''" :content="title">
|
||||
<icon :name="name"></icon>
|
||||
</el-tooltip>
|
||||
<icon v-else :name="name"></icon>
|
||||
</el-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import Icon from './Icon.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import useUserStore from '@/stores/user'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
permission: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const isAllowed = ref(true);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
if (props.permission) {
|
||||
let permission = props.permission;
|
||||
if (permission.indexOf('.') === -1) {
|
||||
permission = route.name + '.' + permission;
|
||||
}
|
||||
const { hasPermission } = useUserStore();
|
||||
if (!hasPermission(permission)) {
|
||||
isAllowed.value = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="audio-element">
|
||||
<div class="audio-toolbar">
|
||||
<div class="audio-control">
|
||||
<icon :name="iconName" :class="playing ? 'playing' : ''" @click="togglePlay"></icon>
|
||||
</div>
|
||||
<span class="audio-duration"> {{ audioDuration }}</span>
|
||||
</div>
|
||||
<el-progress @click="handleSeek" :percentage="percentage" :text-inside="true" :striped="true"
|
||||
:show-text="false"></el-progress>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import Icon from './Icon.vue';
|
||||
import { durationFormat } from '@/assets/js/formatter';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
},
|
||||
done: {
|
||||
type: Function
|
||||
},
|
||||
autoPlay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const playing = ref(false)
|
||||
|
||||
let audioElement = null;
|
||||
|
||||
let audioDuration = ref('');
|
||||
|
||||
let percentage = ref(0);
|
||||
|
||||
let loaded = ref(false);
|
||||
|
||||
const iconClass = computed(() => {
|
||||
if (playing.value) {
|
||||
return 'text-danger'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (playing.value) {
|
||||
return 'pause'
|
||||
} else {
|
||||
return 'play'
|
||||
}
|
||||
})
|
||||
|
||||
const togglePlay = (e) => {
|
||||
try {
|
||||
if (!audioElement.paused) {
|
||||
audioElement.pause();
|
||||
} else {
|
||||
if (loaded.value) {
|
||||
audioElement.play().then(res => {
|
||||
playing.value = true;
|
||||
}).catch(e => {
|
||||
console.log('play error:' + e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeek = (e) => {
|
||||
if (!loaded.value) {
|
||||
return;
|
||||
}
|
||||
let x = e.offsetX;
|
||||
let w = e.target.getBoundingClientRect().width;
|
||||
let p = x / w;
|
||||
let duration = audioElement.duration * p;
|
||||
if (duration > 0 && duration < audioElement.duration) {
|
||||
audioElement.currentTime = Math.floor(duration)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
audioElement = new Audio(props.url);
|
||||
|
||||
audioElement.addEventListener("loadeddata", () => {
|
||||
loaded.value = true;
|
||||
audioDuration.value = durationFormat(audioElement.currentTime, ':') + ' / ' + durationFormat(audioElement.duration, ':');
|
||||
});
|
||||
audioElement.addEventListener("canplaythrough", () => {
|
||||
if (props.autoPlay) {
|
||||
togglePlay();
|
||||
}
|
||||
});
|
||||
audioElement.addEventListener("error", (e) => {
|
||||
audioDuration.value = "加载失败"
|
||||
})
|
||||
audioElement.addEventListener("pause", () => {
|
||||
playing.value = false;
|
||||
});
|
||||
audioElement.addEventListener("ended", () => {
|
||||
if (props.done && typeof props.done === 'function') {
|
||||
playing.value = false;
|
||||
props.done();
|
||||
}
|
||||
});
|
||||
audioElement.addEventListener('timeupdate', () => {
|
||||
let ts = audioElement.currentTime;
|
||||
percentage.value = parseInt((ts / audioElement.duration) * 100);
|
||||
audioDuration.value = durationFormat(ts, ':') + ' / ' + durationFormat(audioElement.duration, ':');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!audioElement.paused) {
|
||||
audioElement.pause();
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div class="avatar-group" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
|
||||
<span v-if="!readonly" @click="handleAdd" class="avatar-plus"
|
||||
:style="'--el-avatar-size:' + size + 'px'">+</span>
|
||||
<el-popover placement="top-start" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<div class="avatar-smaples">
|
||||
<el-avatar v-for="item in userSample" :src="item.url" :title="item.user" :size="size"></el-avatar>
|
||||
</div>
|
||||
</template>
|
||||
<div class="avatar-container">
|
||||
<div class="avatar-list">
|
||||
<el-avatar v-for="item in userLists" :src="item.url" :title="item.user" :size="size*1.2"></el-avatar>
|
||||
</div>
|
||||
<div class="avatar-footer"><span>已选择 : </span> {{ userLists.length }}</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { getUserAvatar } from '@/apis/organize';
|
||||
|
||||
const props = defineProps({
|
||||
users: {
|
||||
type: Array
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['add', 'change'])
|
||||
|
||||
const showAll = ref(false)
|
||||
|
||||
|
||||
const handleMouseEnter = (e) => {
|
||||
showAll.value = true
|
||||
}
|
||||
|
||||
const handleMouseLeave = (e) => {
|
||||
showAll.value = false
|
||||
}
|
||||
|
||||
const userSample = computed(() => {
|
||||
let n = props.users.length;
|
||||
let users = [];
|
||||
if (props.max > 0 && n > props.max) {
|
||||
n = props.max
|
||||
}
|
||||
for (let i = 0; i < n; i++) {
|
||||
users.push({
|
||||
url: getUserAvatar(props.users[i]),
|
||||
user: props.users[i],
|
||||
})
|
||||
}
|
||||
return users;
|
||||
})
|
||||
|
||||
const userLists = computed(() => {
|
||||
let n = props.users.length;
|
||||
let users = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
users.push({
|
||||
url: getUserAvatar(props.users[i]),
|
||||
user: props.users[i],
|
||||
})
|
||||
}
|
||||
return users;
|
||||
})
|
||||
|
||||
const handleAdd = (e) => {
|
||||
emit('add')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch(() => { return props.users }, (val) => {
|
||||
emit('change', val)
|
||||
}, { deep: true })
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<span class="call-widget" @click="handleOffer">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import useUserStore from '@/stores/user'
|
||||
import bus from 'vue3-eventbus'
|
||||
|
||||
const props = defineProps({
|
||||
number: {
|
||||
type: String,
|
||||
}
|
||||
})
|
||||
|
||||
const value = computed(() => {
|
||||
const { hiddlePhone } = useUserStore()
|
||||
if (hiddlePhone) {
|
||||
return invisible(props.number)
|
||||
}
|
||||
return props.number
|
||||
})
|
||||
|
||||
const invisible = (s) => {
|
||||
if (typeof s !== 'string') {
|
||||
return s
|
||||
}
|
||||
if (s.length <= 5) {
|
||||
return s
|
||||
}
|
||||
var reg = /^(\d{3})\d{4}(\d+)$/;
|
||||
if (s.length <= 5) {
|
||||
reg = /^(\d{2})\d{2}(\d+)$/;
|
||||
}
|
||||
return s.replace(reg, "$1****$2");
|
||||
}
|
||||
|
||||
const handleOffer = (e) => {
|
||||
bus.emit("sip_offer", { callee: props.number })
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<i class="iconfont" :class="className"></i>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
})
|
||||
|
||||
const className = computed(() => {
|
||||
if (props.name.indexOf('icon-') > -1) {
|
||||
return props.name
|
||||
} else {
|
||||
return 'icon-' + props.name
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<skeleton :module-name="moduleName" :table-name="tableName" @ready="handleReady">
|
||||
<template #default>
|
||||
<section v-if="isReady" class="upload-container">
|
||||
<div class="upload-panel" v-if="!uploadSuccess">
|
||||
<el-upload drag :action="uploadUrl" :show-file-list="false" :on-success="onUploadSuccess">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖动文件到此或 <em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="outbound-download-template">
|
||||
<span @click="handleDownloadTemplate">如需要模板请<em>点击</em>下载</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-success" v-else>
|
||||
<el-result icon="success" title="上传成功" sub-title="您上传的文件正在为您后台导入, 请您留意系统通知。"></el-result>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</skeleton>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.upload-container {
|
||||
.outbound-download-template {
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
font-size: .7rem;
|
||||
color: var(--el-text-color-muted);
|
||||
cursor: pointer;
|
||||
|
||||
em {
|
||||
text-decoration: none;
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { getBaseUrl } from '@/apis/request';
|
||||
import useUserStore from '@/stores/user';
|
||||
import Skeleton from '@/components/fragment/Skeleton.vue';
|
||||
import queryString from 'query-string';
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
|
||||
let crud = null;
|
||||
|
||||
const props = defineProps({
|
||||
moduleName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
tableName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fields: {
|
||||
type: Array
|
||||
}
|
||||
})
|
||||
|
||||
const isReady = ref(false);
|
||||
|
||||
const uploadSuccess = ref(false);
|
||||
|
||||
const handleReady = (e) => {
|
||||
isReady.value = true;
|
||||
crud = e
|
||||
}
|
||||
|
||||
const uploadUrl = computed(() => {
|
||||
let baseUri = getBaseUrl();
|
||||
let { getAccessToken } = useUserStore()
|
||||
let ps = {
|
||||
'access_token': getAccessToken(),
|
||||
'_': (new Date()).getTime()
|
||||
};
|
||||
//导入附加字段
|
||||
for (let i in props.fields) {
|
||||
let row = props.fields[i];
|
||||
if (!row.column) {
|
||||
continue;
|
||||
}
|
||||
ps['_attr_' + row.column] = row.value;
|
||||
}
|
||||
return baseUri + crud.value.__buildModelUri(props.moduleName, props.tableName, 'import') + "?" + queryString.stringify(ps);
|
||||
})
|
||||
|
||||
const handleDownloadTemplate = (e) => {
|
||||
crud.value.downloadResourceTemplate(props.moduleName, props.tableName);
|
||||
}
|
||||
|
||||
const onUploadSuccess = (response) => {
|
||||
uploadSuccess.value = true;
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<el-divider class="loading-more">
|
||||
<template v-if="running">
|
||||
<el-icon class="is-loading">
|
||||
<loading />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span @click="loadingMore">加载更多</span>
|
||||
</template>
|
||||
</el-divider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
const props = defineProps({
|
||||
callback: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const running = ref(false)
|
||||
|
||||
const loadingMore = () => {
|
||||
if (typeof props.callback === 'function') {
|
||||
running.value = true
|
||||
props.callback().then(res => {
|
||||
running.value = false;
|
||||
}).catch(e => {
|
||||
running.value = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<template v-if="hasChildren(menu.children)">
|
||||
<el-sub-menu :index="menu.route">
|
||||
<template #title>
|
||||
<icon v-if="menu.icon" :name="menu.icon"></icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
</template>
|
||||
<template v-for="child in menu.children">
|
||||
<menu-item v-if="!menu.hidden" :menu="child"></menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-menu-item :index="menu.route" v-if="!menu.hidden">
|
||||
<icon v-if="menu.icon" :name="menu.icon"></icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Icon from './Icon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
menu: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 检查是否有可用的子组件
|
||||
* @param {Array} vs
|
||||
*/
|
||||
const hasChildren = (vs) => {
|
||||
if (!Array.isArray(vs)) {
|
||||
return false;
|
||||
}
|
||||
if (vs.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
let invisible = 0;
|
||||
for (let v of vs) {
|
||||
if (v.hidden) {
|
||||
invisible++
|
||||
}
|
||||
}
|
||||
return invisible < vs.length
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<span class="percentage-label" v-if="text.length > 0">{{ text }}</span>
|
||||
<template v-if="colorful">
|
||||
<el-text :type="displayValue == 0 ? '' : (displayValue < critical ? 'danger' : 'success')">
|
||||
{{ displayValue }}%
|
||||
</el-text>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ displayValue }}%
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
colorful: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
critical: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (props.total === 0 || props.value === 0) {
|
||||
return 0;
|
||||
}
|
||||
let v = ((props.value / props.total) * 100).toFixed(2);
|
||||
v = parseFloat(v);
|
||||
return isNaN(v) ? 0 : v;
|
||||
})
|
||||
|
||||
|
||||
</script>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="preview-item">
|
||||
<div class="preview-item-label">
|
||||
<slot name="label" :label="label">
|
||||
<label class=" text-muted"> {{ label }}</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="preview-item-value">
|
||||
<slot name="value" :label="label">
|
||||
<span>{{ value }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: [String, Number]
|
||||
},
|
||||
value: {
|
||||
type: [String, Number]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.preview-item {
|
||||
overflow: hidden;
|
||||
font-size: .9rem;
|
||||
margin-bottom: .68rem;
|
||||
clear: both;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.preview-item-label {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
min-width: 4.2em;
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
.preview-item-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
word-break: normal;
|
||||
word-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<span class="colorful-tag" :style="{ color: textColor, backgroundColor: backgroundColor }">
|
||||
<slot name="default">
|
||||
{{ text }}
|
||||
</slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.colorful-tag {
|
||||
display: inline-block;
|
||||
font-size: .78rem;
|
||||
padding: .12rem .6rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Color from 'color';
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
}
|
||||
})
|
||||
|
||||
const textColor = computed(() => {
|
||||
return props.color
|
||||
})
|
||||
|
||||
const backgroundColor = computed(() => {
|
||||
return Color(props.color).fade(0.8).hsl()
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<el-cascader v-model="value" :options="options" :show-all-levels="false" @change="handleChange" :clearable="clearable" filterable/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getDepartmentUserNested } from '@/apis/organize'
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const value = ref([]);
|
||||
|
||||
const options = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const hasUser = (uid, users) => {
|
||||
if (!Array.isArray(users)) {
|
||||
return false;
|
||||
}
|
||||
for (let i in users) {
|
||||
if (users[i].value == uid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
if(!Array.isArray(e)){
|
||||
emit('update:modelValue', '');
|
||||
return
|
||||
}
|
||||
if (e.length > 1) {
|
||||
emit('update:modelValue', e[e.length - 1]);
|
||||
} else {
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDepartmentUserNested().then(res => {
|
||||
options.value = res;
|
||||
for (let i in options.value) {
|
||||
if (!Array.isArray(options.value[i].children) || options.value[i].children.length == 0) {
|
||||
options.value[i].disabled = true;
|
||||
}
|
||||
}
|
||||
if (props.modelValue != '') {
|
||||
for (let i in res) {
|
||||
let row = res[i];
|
||||
if (hasUser(props.modelValue, row.children)) {
|
||||
value.value = [row.value, props.modelValue];
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,239 @@
|
|||
<template>
|
||||
<div class="d-flex user-pick " :style="{ 'height': contentHeight }">
|
||||
<div class="department-panel">
|
||||
<el-tree :data="departments" @node-click="handleChangeDepartment" />
|
||||
</div>
|
||||
<div class="user-panel">
|
||||
<template v-if="users.length > 0">
|
||||
<div class="user-panel-action">
|
||||
<el-checkbox v-model="isSelectedAll" :indeterminate="isIndeterminate"
|
||||
@change="handleToggleAll">全选</el-checkbox>
|
||||
</div>
|
||||
<div v-for="item in users" class="user-pick-user d-flex align-center"
|
||||
:class="item.selected ? 'active' : ''">
|
||||
<div class="flex-shrink" @click="handleUserClicked(item)">
|
||||
<el-avatar :size="26" :src="item.avatar" :title="item.username"></el-avatar>
|
||||
</div>
|
||||
<div class="flex-fill" @click="handleUserClicked(item)">
|
||||
<span class="px-1">{{ item.username }} ({{ item.uid }})</span>
|
||||
</div>
|
||||
<div class="flex-shrink">
|
||||
<el-checkbox v-model="item.selected" @change="handleUserChecked(item)"></el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-empty></el-empty>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-pick-status"><span>已选择:</span>{{ props.modelValue.length }}</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.user-pick-status {
|
||||
padding-top: .68rem;
|
||||
font-size: .8rem;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-pick {
|
||||
--user-pick-border: var(--el-border-color-lighter);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--user-pick-border);
|
||||
border-radius: 6px;
|
||||
|
||||
.flex-shrink {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-panel {
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
flex: 1 1 auto !important;
|
||||
overflow-y: auto;
|
||||
|
||||
.user-panel-action {
|
||||
text-align: right;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-pick-user {
|
||||
box-sizing: border-box;
|
||||
padding: .2rem .5rem;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-muted);
|
||||
|
||||
&.active {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.department-panel {
|
||||
box-sizing: border-box;
|
||||
padding: 1rem 0rem;
|
||||
background-color: var(--el-fill-color);
|
||||
min-width: 120px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
--el-fill-color-blank: transparent;
|
||||
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
padding: .38rem 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all .3s ease;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
span {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: .9rem;
|
||||
color: var(--el-text-color-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
import { getNestedDepartments, getDepartmentUsers } from '@/apis/organize'
|
||||
import { getBaseUrl } from '@/apis/request';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Icon from './Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 400,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'update:modelValue'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const departments = ref([])
|
||||
|
||||
const users = ref([])
|
||||
|
||||
const isIndeterminate = ref(false);
|
||||
|
||||
const isSelectedAll = ref(false);
|
||||
|
||||
const activeDepartment = ref('');
|
||||
|
||||
const contentHeight = computed(() => {
|
||||
if (typeof props.height === 'number') {
|
||||
return props.height + 'px'
|
||||
}
|
||||
return props.height
|
||||
})
|
||||
|
||||
const changeUser = (e) => {
|
||||
let selected = props.modelValue;
|
||||
if (e.selected) {
|
||||
if (selected.indexOf(e.uid) === -1) {
|
||||
selected.push(e.uid);
|
||||
}
|
||||
} else {
|
||||
let pos = selected.indexOf(e.uid);
|
||||
if (pos > -1) {
|
||||
selected.splice(pos, 1)
|
||||
}
|
||||
}
|
||||
if (selected.length == users.value.length || selected.length == 0) {
|
||||
isIndeterminate.value = false
|
||||
} else {
|
||||
isIndeterminate.value = true
|
||||
}
|
||||
if (selected.length == users.value.length) {
|
||||
isSelectedAll.value = true
|
||||
}
|
||||
emit('change', e)
|
||||
emit('update:modelValue', selected);
|
||||
}
|
||||
|
||||
const handleUserClicked = (e) => {
|
||||
e.selected = !e.selected;
|
||||
changeUser(e)
|
||||
}
|
||||
|
||||
const handleUserChecked = (e) => {
|
||||
changeUser(e)
|
||||
}
|
||||
|
||||
const handleToggleAll = (e) => {
|
||||
if (isSelectedAll.value) {
|
||||
for (let i in users.value) {
|
||||
users.value[i].selected = true;
|
||||
changeUser(users.value[i]);
|
||||
}
|
||||
} else {
|
||||
for (let i in users.value) {
|
||||
users.value[i].selected = false;
|
||||
changeUser(users.value[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangeDepartment = (e) => {
|
||||
loading.value = true;
|
||||
activeDepartment.value = e.value;
|
||||
getDepartmentUsers(e.value).then(res => {
|
||||
users.value = []
|
||||
isSelectedAll.value = false;
|
||||
for (let i in res) {
|
||||
let row = res[i];
|
||||
if (row.avatar.indexOf('//') == -1) {
|
||||
row.avatar = getBaseUrl() + row.avatar;
|
||||
}
|
||||
if (props.modelValue.indexOf(row.uid) > -1) {
|
||||
row.selected = true
|
||||
} else {
|
||||
row.selected = false;
|
||||
}
|
||||
users.value.push(row);
|
||||
}
|
||||
if (props.modelValue.length == 0 || props.modelValue.length == users.value.length) {
|
||||
isIndeterminate.value = false
|
||||
} else {
|
||||
isIndeterminate.value = true
|
||||
}
|
||||
if (props.modelValue.length == users.value.length) {
|
||||
isSelectedAll.value = true
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(e => {
|
||||
users.value = []
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getNestedDepartments().then(res => {
|
||||
departments.value = res;
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,708 @@
|
|||
const menu = [
|
||||
{
|
||||
label: "控制面板",
|
||||
icon: "dashboard",
|
||||
hidden: false,
|
||||
route: "/dashboard",
|
||||
children: [
|
||||
{
|
||||
label: "字段配置",
|
||||
hidden: true,
|
||||
access: 'allow',
|
||||
route: "/setting/schemas"
|
||||
},
|
||||
{
|
||||
label: "消息通知",
|
||||
hidden: true,
|
||||
access: 'allow',
|
||||
route: "/organize/user/notice"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "组织架构",
|
||||
icon: "org",
|
||||
hidden: false,
|
||||
route: '/organizers',
|
||||
children: [
|
||||
{
|
||||
label: "个人设置",
|
||||
hidden: true,
|
||||
access: 'allow',
|
||||
route: "/organize/user/profile"
|
||||
},
|
||||
{
|
||||
label: "角色管理",
|
||||
route: "/organize/roles",
|
||||
permissions: [
|
||||
{
|
||||
label: '新建',
|
||||
value: 'create'
|
||||
},
|
||||
{
|
||||
label: '更新',
|
||||
value: 'update'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
value: 'delete'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "部门管理",
|
||||
route: "/organize/departments",
|
||||
permissions: [
|
||||
{
|
||||
label: '新建',
|
||||
value: 'create'
|
||||
},
|
||||
{
|
||||
label: '更新',
|
||||
value: 'update'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
value: 'delete'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "座席管理",
|
||||
route: "/organize/users",
|
||||
permissions: [
|
||||
{
|
||||
label: '新建',
|
||||
value: 'create'
|
||||
},
|
||||
{
|
||||
label: '更新',
|
||||
value: 'update'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
value: 'delete'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
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;
|
|
@ -0,0 +1,26 @@
|
|||
import useUserStore from '@/stores/user'
|
||||
const installPermission = (app) => {
|
||||
app.directive("permission", {
|
||||
mounted: function (el, binding) {
|
||||
const { hasPermission } = useUserStore();
|
||||
let permission = binding.value;
|
||||
if (typeof permission !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (permission.indexOf('.') === -1) {
|
||||
if (app.config.globalProperties && app.config.globalProperties.$route) {
|
||||
permission = app.config.globalProperties.$route.name + '.' + permission;
|
||||
}
|
||||
}
|
||||
if (!hasPermission(permission)) {
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
install: installPermission
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
const messages = {}
|
||||
|
||||
|
||||
export default messages
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<div class="d-flex header-wrapper" :style="{ backgroundColor: headerBackgroundColor }">
|
||||
<div class="logo flex-fill">
|
||||
<img :src="logoUrl" class="hidden-sm-and-down" :title="productName" />
|
||||
<icon name="icon-unorderedlist" class="hidden-md-and-up" @click="handleToggleMenuVisible"></icon>
|
||||
</div>
|
||||
<div class="flex-shrink">
|
||||
|
||||
</div>
|
||||
<div class="flex-shrink">
|
||||
<ul class="list list-inline">
|
||||
<li>
|
||||
<icon name="fullscreen" @click="handleFullscreen"></icon>
|
||||
</li>
|
||||
<li>
|
||||
<el-badge :value="unreadMsgCount" :hidden="unreadMsgCount <= 0">
|
||||
<icon name="message" @click="handleListNotices"></icon>
|
||||
</el-badge>
|
||||
</li>
|
||||
<li style="display: none;">
|
||||
<el-dropdown @command="handleChangeLang">
|
||||
<icon name="network"></icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-CN">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en-US">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<el-dropdown @command="handleMenuCommand">
|
||||
<div class="header-avatar">
|
||||
<el-avatar :size="36" :title="username" :src="avatar">
|
||||
</el-avatar>
|
||||
<i class="user-status" :style="{ backgroundColor: userStateColor }" :title="userStateText"
|
||||
@click="dialogVisible = true"></i>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="status">设置状态</el-dropdown-item>
|
||||
<el-dropdown-item command="profile">个人设置</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出系统</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog class="status-dialog" v-model="dialogVisible" width="320px" draggable>
|
||||
<div class="current-status">
|
||||
<div>
|
||||
<i :style="{ backgroundColor: userStateColor }"></i> <span>{{ userStateText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="item in userStatus">
|
||||
<div class="status-item" @click="handleSetStatus(item)"
|
||||
:class="item.value === userState ? 'active' : ''">
|
||||
<div class="status-avatar">
|
||||
<i :style="{ backgroundColor: item.color }"></i>
|
||||
</div>
|
||||
<div class="status-text">{{ item.label }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, h, inject, onMounted, onUnmounted, ref } from 'vue';
|
||||
import useSystemStore from '@/stores/system'
|
||||
import useThemeStore from '@/stores/theme'
|
||||
import useUserStore from '@/stores/user'
|
||||
import { storeToRefs } from 'pinia';
|
||||
import screenfull from 'screenfull';
|
||||
import Icon from '@/components/widgets/Icon.vue';
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getUserStatus, getStatusText, getStatusTextColor } from '@/assets/js/status'
|
||||
import { ElNotification } from 'element-plus';
|
||||
import Notice from './parts/Notice.vue';
|
||||
|
||||
const systemStore = useSystemStore();
|
||||
const themeStore = useThemeStore();
|
||||
const userStore = useUserStore();
|
||||
const userState = ref('idle');
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const { logoUrl, productName } = storeToRefs(systemStore);
|
||||
const { headerBackgroundColor } = storeToRefs(themeStore);
|
||||
const { uid, avatar, username, unreadMsgCount } = storeToRefs(userStore);
|
||||
|
||||
|
||||
const logout = inject('logout');
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const userStatus = computed(() => {
|
||||
return getUserStatus();
|
||||
})
|
||||
|
||||
const userStateText = computed(() => {
|
||||
return getStatusText(userState.value)
|
||||
})
|
||||
|
||||
const userStateColor = computed(() => {
|
||||
return getStatusTextColor(userState.value)
|
||||
})
|
||||
|
||||
const handleFullscreen = (e) => {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
const handleListNotices = (e) => {
|
||||
let uid = userStore.uid;
|
||||
router.push(`/organize/user/notice?receiver=${uid}&read=no`)
|
||||
}
|
||||
|
||||
const handleChangeLang = (e) => {
|
||||
systemStore.setLanguage(e)
|
||||
}
|
||||
|
||||
const handleToggleMenuVisible = (e) => {
|
||||
systemStore.toggleFlowSidebarVisible()
|
||||
}
|
||||
|
||||
const handleSetStatus = (e) => {
|
||||
|
||||
}
|
||||
|
||||
const handleMenuCommand = (e) => {
|
||||
switch (e) {
|
||||
case 'status':
|
||||
dialogVisible.value = true;
|
||||
break;
|
||||
case 'profile':
|
||||
router.push('/organize/user/profile')
|
||||
break
|
||||
case 'logout':
|
||||
logout();
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const refreshUnreadMsgCount = () => {
|
||||
|
||||
}
|
||||
|
||||
const doUserStatusChange = (e) => {
|
||||
if (e.data) {
|
||||
userState.value = e.data.state;
|
||||
}
|
||||
}
|
||||
|
||||
const doUserNotice = (e) => {
|
||||
let data = e.data;
|
||||
let notice = ElNotification({
|
||||
title: data.title,
|
||||
position: 'bottom-right',
|
||||
message: h(Notice, { data: data, onDone: () => { notice.close() } })
|
||||
})
|
||||
refreshUnreadMsgCount();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshUnreadMsgCount();
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<el-skeleton animated :loading="loading" :throttle="2000">
|
||||
<template #template>
|
||||
<div class="skeleton" v-loading="loading"></div>
|
||||
</template>
|
||||
<el-container class="page" :style="{ backgroundColor: backgroundColor }" v-if="!loading">
|
||||
<el-header>
|
||||
<headerbar></headerbar>
|
||||
</el-header>
|
||||
<el-container class="container">
|
||||
<el-aside :width="sidebarWidth + 'px'" :class="collapse ? 'collapse' : ''"
|
||||
:style="{ backgroundColor: sidebarBackgroundColor }" class="hidden-sm-and-down">
|
||||
<sidebar></sidebar>
|
||||
</el-aside>
|
||||
<el-drawer v-model="flowSidebarVisible" class="hidden-md-and-up sidebar-drawer" direction="ltr"
|
||||
:size="220" :style="{ backgroundColor: sidebarBackgroundColor }">
|
||||
<sidebar></sidebar>
|
||||
</el-drawer>
|
||||
<el-container>
|
||||
<el-main>
|
||||
<viewer></viewer>
|
||||
</el-main>
|
||||
<el-footer class="hidden-sm-and-down">
|
||||
<statusbar></statusbar>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Headerbar from './Headerbar.vue'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import Viewer from './Viewer.vue'
|
||||
import Statusbar from './Statusbar.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted, onUnmounted, provide, nextTick } from 'vue'
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useThemeStore from '@/stores/theme'
|
||||
import useSystemStore from '@/stores/system'
|
||||
import useUserStore from '@/stores/user'
|
||||
import { getBaseHost } from '@/apis/request'
|
||||
import { updateStatusMap } from '@/assets/js/status'
|
||||
import { userLogout, getUserProfile } from '@/apis/organize'
|
||||
import { getConfigure } from '@/apis/system'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const viewVisible = ref(true)
|
||||
|
||||
const { productName, collapse, sidebarWidth, flowSidebarVisible } = storeToRefs(systemStore);
|
||||
|
||||
const { backgroundColor, sidebarBackgroundColor } = storeToRefs(themeStore);
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const redirectTo = (uri) => {
|
||||
viewVisible.value = false;
|
||||
nextTick(() => {
|
||||
if (typeof uri === 'string') {
|
||||
if (uri[0] === '/') {
|
||||
uri = uri.substring(1)
|
||||
}
|
||||
}
|
||||
router.push(`/redirect/${uri}`)
|
||||
viewVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
provide("redirectTo", redirectTo);
|
||||
|
||||
const logout = () => {
|
||||
userLogout().then(res => {
|
||||
const { setAccessToken } = useUserStore()
|
||||
setAccessToken('', 0);
|
||||
nextTick(() => {
|
||||
router.push('/login')
|
||||
})
|
||||
}).catch(e => { })
|
||||
}
|
||||
|
||||
provide('logout', logout)
|
||||
|
||||
onMounted(() => {
|
||||
document.title = productName.value
|
||||
let schema = window.location.protocol == 'http:' ? "ws://" : 'wss://'
|
||||
//更新用户信息
|
||||
getConfigure().then(res => {
|
||||
const { setAttributeValue } = useSystemStore();
|
||||
for (let i in res) {
|
||||
let row = res[i];
|
||||
setAttributeValue(row.attribute, row.value);
|
||||
}
|
||||
}).catch(e => {
|
||||
console.log(e);
|
||||
router.push('/login');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="d-flex aside-wrapper">
|
||||
<div class="flex-fill">
|
||||
<el-menu default-active="2" :background-color="sidebarBackgroundColor" :text-color="sidebarTextColor"
|
||||
:active-text-color="sidebarActiveTextColor" :router="true" :collapse="collapse">
|
||||
<template v-for="menu in rawMenus">
|
||||
<menu-item v-if="!menu.hidden" :menu="menu"></menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="flex-shrink drag-bar" :class="dragging ? 'active' : ''" @mousedown="handleDragStart">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuItem from '@/components/widgets/MenuItem.vue'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useThemeStore from '@/stores/theme'
|
||||
import useSystemStore from '@/stores/system'
|
||||
import useUserStore from '@/stores/user'
|
||||
import { filterMenus,getMenus } from '@/assets/js/menu'
|
||||
const themeStore = useThemeStore();
|
||||
const systemStore = useSystemStore();
|
||||
const userStore = useUserStore();
|
||||
const { sidebarBackgroundColor, sidebarTextColor, sidebarActiveTextColor } = storeToRefs(themeStore);
|
||||
const { collapse, sidebarWidth } = storeToRefs(systemStore)
|
||||
|
||||
|
||||
const dragging = ref(false)
|
||||
|
||||
const navbarOffset = ref(0)
|
||||
|
||||
const asideBeforeDragWidth = ref(0)
|
||||
|
||||
const rawMenus = computed(() => {
|
||||
const { permissions } = userStore
|
||||
return filterMenus(permissions);
|
||||
})
|
||||
|
||||
const handleDragStart = (e) => {
|
||||
dragging.value = true
|
||||
navbarOffset.value = e.clientX
|
||||
asideBeforeDragWidth.value = sidebarWidth.value
|
||||
}
|
||||
|
||||
|
||||
const handleDragging = (e) => {
|
||||
if (!dragging.value) {
|
||||
return
|
||||
}
|
||||
let offset = e.clientX - navbarOffset.value;
|
||||
sidebarWidth.value = asideBeforeDragWidth.value + offset;
|
||||
if (sidebarWidth.value < 68 && offset < 0) {
|
||||
collapse.value = true;
|
||||
dragging.value = false;
|
||||
sidebarWidth.value = 68;
|
||||
}
|
||||
if (sidebarWidth.value > 100 && offset > 0) {
|
||||
if (collapse.value) {
|
||||
collapse.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragged = (e) => {
|
||||
dragging.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', handleDragging)
|
||||
document.addEventListener('mouseup', handleDragged)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleDragging)
|
||||
document.removeEventListener('mouseup', handleDragged)
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div class="d-flex align-center">
|
||||
<div class="flex-shrink">
|
||||
</div>
|
||||
<div class="flex-fill"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<RouterView></RouterView>
|
||||
</template>
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<ul class="list list-inline">
|
||||
<!--
|
||||
<li class="hidden-sm-and-down">
|
||||
<el-input :prefix-icon="Search" v-model="qs" clearable class="round"></el-input>
|
||||
</li>
|
||||
-->
|
||||
<li>
|
||||
<el-popover placement="bottom" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<icon name="phone"></icon>
|
||||
</template>
|
||||
<div class="dialplate" @keydown="handleKeypress">
|
||||
<div class="dialplate-input">
|
||||
<el-input v-model="number" placeholder="请输入要拨打的号码" readonly></el-input>
|
||||
</div>
|
||||
<div class="dialplate-clear">
|
||||
<el-icon>
|
||||
<Close v-if="number.length > 0" @click="handleClearInput"></Close>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="dialplate-button" @click="handleInput(item)" v-for="item in dialNumbers">{{ item }}
|
||||
</div>
|
||||
<div class="dialplate-actions">
|
||||
<el-button type="success" circle @click="handleOffer">
|
||||
<icon name="phone"></icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</li>
|
||||
<li v-if="inWrapUp">
|
||||
<el-tooltip content="结束整理">
|
||||
<icon name="wrapup" class="text-danger" @click="handleEndWraup"></icon>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li v-if="inCalling">
|
||||
<el-tooltip content="保持">
|
||||
<icon name="muted" class="text-warning" @click="handleToggleHold"></icon>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li v-if="canHanguped">
|
||||
<el-tooltip content="挂机">
|
||||
<icon name="phone" class="text-danger" @click="handleHangup"></icon>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
<Teleport to="body">
|
||||
<popup v-model="dialogVisible" :model="call"></popup>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, provide, ref } from 'vue'
|
||||
import Connection from '@/assets/js/connection'
|
||||
import Popup from './Popup.vue'
|
||||
import Sip from '@/assets/js/sip'
|
||||
import useUserStore from '@/stores/user'
|
||||
import Icon from '@/components/widgets/Icon.vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { nextID, hangup, endWrapUp } from '@/apis/control'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { getBaseHost } from '@/apis/request'
|
||||
import bus from 'vue3-eventbus'
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const call = ref({
|
||||
id: '',
|
||||
direction: '',
|
||||
caller: '',
|
||||
callee: '',
|
||||
province: '',
|
||||
city: '',
|
||||
status: 'hanguped',
|
||||
modtime: 0,
|
||||
})
|
||||
|
||||
const qs = ref('')
|
||||
|
||||
const number = ref('')
|
||||
|
||||
const inWrapUp = ref(false);
|
||||
|
||||
const inCalling = computed(() => {
|
||||
return call.value.status === 'answered'
|
||||
})
|
||||
|
||||
const canHanguped = computed(() => {
|
||||
return call.value.status !== 'hanguped'
|
||||
})
|
||||
|
||||
const dialNumbers = computed(() => {
|
||||
return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
|
||||
})
|
||||
|
||||
/**
|
||||
* 建立一个呼叫
|
||||
* @param {String} caller
|
||||
* @param {String} callee
|
||||
* @param {String} direction
|
||||
*/
|
||||
const setupCall = (caller, callee, direction) => {
|
||||
call.value.caller = caller;
|
||||
call.value.callee = callee;
|
||||
call.value.direction = direction;
|
||||
call.value.modtime = (new Date()).getTime() / 1000;
|
||||
call.value.status = 'setup';
|
||||
if (!dialogVisible.value) {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新一个呼叫
|
||||
* @param {String} status
|
||||
*/
|
||||
const updateCall = (status) => {
|
||||
call.value.status = status;
|
||||
call.value.modtime = (new Date()).getTime() / 1000;
|
||||
if (status === 'hanguped') {
|
||||
call.value.id = '';
|
||||
if (dialogVisible.value) {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const updateCallStatus = (incall) => {
|
||||
let appElement = document.getElementById('app');
|
||||
if (appElement) {
|
||||
if (incall) {
|
||||
appElement.className = 'call-answering'
|
||||
} else {
|
||||
appElement.className = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeypress = (e) => {
|
||||
if (dialNumbers.value.indexOf(e.key) > -1) {
|
||||
number.value += e.key
|
||||
if (call.value.status === 'answered') {
|
||||
Sip.sendDTMF(e.key).then(res => { }).catch(e => { })
|
||||
}
|
||||
} else {
|
||||
if (e.keyCode === 8) {
|
||||
if (number.value.length > 0) {
|
||||
number.value = number.value.substring(0, number.value.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (e) => {
|
||||
number.value += e
|
||||
if (call.value.status === 'answered') {
|
||||
Sip.sendDTMF(e).then(res => { }).catch(e => { })
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearInput = (e) => {
|
||||
if (number.value.length > 0) {
|
||||
number.value = number.value.substring(0, number.value.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const handleCallEvent = e => {
|
||||
// 这里看后端推送的消息不做任何处理,全部变由sip信令进行触发
|
||||
|
||||
// let data = e.data;
|
||||
// switch (e.event) {
|
||||
// case 'CALL_CREATE':
|
||||
// call.value.id = data.id;
|
||||
// call.value.caller = data.caller;
|
||||
// call.value.callee = data.callee;
|
||||
// call.value.direction = data.direction;
|
||||
// call.value.province = data.province;
|
||||
// call.value.city = data.city;
|
||||
// call.value.status = 'setup';
|
||||
// call.value.modtime = (new Date()).getTime() / 1000;
|
||||
// if (!data.forwarded) {
|
||||
// if (!dialogVisible.value) {
|
||||
// dialogVisible.value = true;
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// case 'CALL_BRIDGE':
|
||||
// updateCall('answered');
|
||||
// updateCallStatus(true);
|
||||
// break;
|
||||
// case 'CALL_HANGUP':
|
||||
// updateCall('hanguped');
|
||||
// updateCallStatus(false);
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
|
||||
const handleStateEvent = e => {
|
||||
if (!e.data) {
|
||||
return;
|
||||
}
|
||||
if (e.data.state === 'wrapUp') {
|
||||
inWrapUp.value = true;
|
||||
} else {
|
||||
inWrapUp.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const sipOffer = (callee) => {
|
||||
const { uid } = useUserStore()
|
||||
nextID().then(res => {
|
||||
Sip.offer(callee, { 'sip_h_X-call_id': res.id }).then(res => {
|
||||
setupCall(uid, callee, 'outbound');
|
||||
ElMessage.success('发起成功')
|
||||
number.value = '';
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message);
|
||||
})
|
||||
}).catch(e => {
|
||||
Sip.offer(callee).then(res => {
|
||||
setupCall(uid, callee, 'outbound');
|
||||
ElMessage.success('发起成功')
|
||||
number.value = '';
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
provide('sipOffer', sipOffer);
|
||||
|
||||
const handleOffer = e => {
|
||||
const callee = number.value;
|
||||
sipOffer(callee);
|
||||
}
|
||||
|
||||
const handleEndWraup = e => {
|
||||
const { uid } = useUserStore()
|
||||
endWrapUp(uid).then(res => {
|
||||
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggleHold = e => {
|
||||
|
||||
}
|
||||
|
||||
const handleHangup = e => {
|
||||
const { uid } = useUserStore()
|
||||
if (call.value.status === 'setup' && call.value.direction === 'inbound' && Sip.getIsRegistered()) {
|
||||
Sip.decline().then(res => {
|
||||
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
} else {
|
||||
if (Sip.getIsRegistered()) {
|
||||
Sip.hangup().then(res => {
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
} else {
|
||||
hangup(uid).then(res => {
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { uid, getPassword, getAccessToken, setDeviceRegisterState, getAutoLoginDevice } = useUserStore()
|
||||
let domain = 'test.cc.echo.me';
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
domain = window.location.hostname;
|
||||
}
|
||||
if (!getAutoLoginDevice()) {
|
||||
return;
|
||||
}
|
||||
//如果登录设备才进行连接,否则的话不进行登录
|
||||
Sip.connect(getBaseHost(), {
|
||||
domain: domain,
|
||||
urlPath: '/sip',
|
||||
debug: false,
|
||||
video: false,
|
||||
qs: {
|
||||
'access_token': getAccessToken(),
|
||||
}
|
||||
})
|
||||
|
||||
Sip.register(uid, getPassword()).then(res => {
|
||||
|
||||
}).catch(e => {
|
||||
console.log(e);
|
||||
})
|
||||
|
||||
Sip.on('EVENT_DEVICE_REGISTERED', (e) => {
|
||||
setDeviceRegisterState(true)
|
||||
})
|
||||
|
||||
Sip.on('EVENT_DEVICE_UNREGISTERED', (e) => {
|
||||
setDeviceRegisterState(false)
|
||||
})
|
||||
|
||||
Sip.on('EVENT_CALL_RECEIVED', (e) => {
|
||||
setupCall(e.caller, e.callee, e.direction);
|
||||
})
|
||||
|
||||
Sip.on('EVENT_CALL_ANSWER', e => {
|
||||
if (!call.value.id) {
|
||||
number.value = '';
|
||||
updateCall('answered');
|
||||
}
|
||||
})
|
||||
|
||||
Sip.on('EVENT_CALL_HANGUP', e => {
|
||||
if (!call.value.id) {
|
||||
number.value = '';
|
||||
updateCall('hanguped');
|
||||
}
|
||||
})
|
||||
|
||||
bus.on("sip_offer", e => {
|
||||
if (typeof (e) === 'object') {
|
||||
if (e.callee && typeof e.callee === 'string') {
|
||||
sipOffer(e.callee);
|
||||
}
|
||||
}
|
||||
})
|
||||
Connection.on('call_event', handleCallEvent)
|
||||
Connection.on('user_state', handleStateEvent)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
Connection.off('call_event', handleCallEvent)
|
||||
Connection.on('user_state', handleStateEvent)
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div class="notice-container">
|
||||
<div class="notice-body">
|
||||
{{ data.content }}
|
||||
</div>
|
||||
<div class="notice-footer">
|
||||
<el-button type="primary" @click="handleReadMsg">收到</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['done'])
|
||||
|
||||
const handleReadMsg = (e) => {
|
||||
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,229 @@
|
|||
<template>
|
||||
<div class="popup-dialog" :style="{ left: position.left + 'px', top: position.top + 'px' }" v-if="modelValue"
|
||||
:class="needAnswer ? 'noanswer' : ''">
|
||||
<div class="popup-header" @mousedown="handleDragStart">
|
||||
<span class="text-muted cursor-pointer" @click="handleClose">×</span>
|
||||
</div>
|
||||
<div class="popup-container">
|
||||
<div class="popup-avatar">
|
||||
<el-avatar :size="64" :src="avatar">
|
||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAACCJJREFUeAHtnUlvFEcUx1/P5rHx7vEy3o3B2EzCkoUTpxws8T245MoHyS3iwveI5APKgRMKEkSxkJAiEtZgDIF4ADMwTOrf0NHMZLaa7qrq17wnjQb3dNd79f911avqrm48arJr12+WqpXqRbV5u0a0TFQbbtpF/oyVAl7ZI7qnQtpJ59JXzp87s1sfnvrto+3u7ub2/z78gTz6vlarpYLt8s1HAc/zPlCNLhcm8pdKpVIFkfuAfbgvDn9SYL/jUx2JtJ0CCvTVwnj+AiD7LRUtV+C2k4vfdrD0e2MVuufn3HfVX6Vb5geyU8TortPZ9KkMBlQ1kpzbSSyOv6HBgi266G2OFZCYe1JgO/VxKtTTzrITMwXAVrVgmecy46YRbm3YH0VrHCG7MlNAADMDphuuANZVjNn+ApgZMN1wBbCuYsz2F8DMgOmGK4B1FWO2vwBmBkw3XAGsqxiz/QUwM2C64QpgXcWY7S+AmQHTDVcA6yrGbH8BzAyYbrgCWFcxZvtnmMWrHe7oyDAVpsZpZHiIcrksDagP7G3lHVXU56D8mvafvaB/DsraZXM4IJGA1YIzWpyfUZ9ZH2orEEODaRoazNP42AgtLcz6sB88ekIPHu2RWs/U6hCW2xIHGK11fW2J8gM5LSBo3UdXF2m+OEO/373vt2qtAmK6c6Jy8OryPJU217Xh1rPBiYEyUFYSLDGAtzbWaGWpGBkTlIUyuVsiAKO1zUxPRs4CZXJvyewBI+dG2XKbzxKUDR9cjTVgjJYxoDJt8AFfHI01YEyFdEfL/UCCD/jiaMwBz1rTHHNqjsYWMK5QYe5qy+ALPrkZW8AuBj4ufIY9odgCxrVl2+bCZ9g6sgVss3sORHbhM/Dd7zdbwMFdoX4r3s9xLnz2E2f9MWwB11fC1r853mRiCxj3c21b5Z19n2HryBYwbtbbNhc+w9aRLWCsxLBtB+VXtl2G9scWMJbZ2Lb9Zy9tuwztjy1grKGy2WXCF8d1W2wB49TGGipbZtNXlHViDniPDt/679yMUpP/lQUfWIzH0VgDxupHLJAzbfDBdaUla8AAi8HWn/cfG2OMsl0M6KKqEHvAEOKPe49o7+nzqDT5rxyUibI5WyIAA8DtO3cjbclouSiTuyVq4TtaW/nV674WvgcgMaBK0sL3RAEGJOTLZ89fdn10JQAafGOeK4+uBGrE/Bsj3vsPn/gfefgs5rDChoerTxyvQIWtd3B8YgZZQYXku1EBAdyoR+L+EsCJQ9pYIbaAUymP5mYL9PWZLZoYH22sVYR/DQ3lKZ1OR1ii3aLYTZOwsnGhOE3F2WnKZj+Gv7mxSjdu3o789iF8nf5ig1Jeih4+3vOnUe/fV+0SCunN+/naLyzeV5BKpfynCPG6hVYPgr14eUC3frsTUo7GwwEXr3gIrFqtKtBP1fTrL+ICmkUXjS74m7MnaXlxriVcAACIKJ/lRVn1cOEDXTVi+PZsiSYnxrAp9hZrwJlMhjaPr9Kp0nEazA90FRPiR5GPUQbKamfour88eYw21pcJPUucLbbRDao34Hx1epNmZ6Z61g9dN/JxmCcQcCzKaJUGmgMpzk37PUucH2mJJeCx0WE6e+pET622WfRcNhvq3Rp4LwfK6NXQszTn6l6PtbFf7ABPFyb8Ljmruud+rd983Crv9hIDcjO67MJk/F71ECvAmNeiBUWR13Tzcbe82w00Yj65eVQrpXQrM4rfYwMYrQ6Dll5yXy8V18nHOnm3k2/4PHFshaZi1JJjARgDqpI6+6OCG0DoNR/r5t2g/Fbf/omlRv54TWIczDlgTIWQv/Btwrrl437zbqdYM5k0lbbWY3GJ0zngLTUl6WWO20nQbr+1y8dh824nv2jBcXhTnlPAGDHbuCLUKh9HlXc7QZ6aHKM5jXl8p7L6/c0Z4HQ6Revq7a62rDkfR5l3O9VhbWVBddXOZCZnnleXF2hA85W/nYTs5bcgH5vIu+38o6dYWXL35lozI5t2tf20/cjQoH/Lr8tuRn7udI3ZiENV6IJ6B/XjJ/v05s2hKRdty3XSghfb3PJrG2WEPyAfRz0d6xYeFiesqbtTLsw6YEwhZtTg6nMzXPwIFijYrLt1wEV1OTKKS5E2RYrCl7/EaKYQRVFaZVgHPK9usX2uVpxLOGBcWMj3cOM+qScALuhgJG/TrLbgiXG7lbMpZK++bN+IsAp4bFQAYzGDTbMGGAOrOC9tsSX68JFBq4NMa4Bx5tqef9qCpuMHGoyNHNE5JNS+VgGHijRBB49a7KatAbZ93TnO54Pp26P1dbcGOMwiuvqAk/Bvm1e07AH+9BxREgCFrYMADqtgzI+32ZvZa8GG1lzFnGXL8Gy2YGv3g2/cut2ysrLRrALWAB8evjVbEym9pQLWuuiW3mWjcQUEsHGJ3ToQwG71N+5dABuX2K0DAexWf+PeBbBxid06EMBu9TfuXQAbl9itAwHsVn/j3gWwcYndOhDAbvU37l0AG5fYrQMB7FZ/494FsHGJ3ToQwG71N+5dABuX2K0DAexWf+PeBbBxid06EMBu9TfuXQAbl9itAwXYK7sNQbybU8Arq/e/0D1zDqRklwqALbroHZdBiG+jCuyk0rn0FfXM6gejbqRw6wqAKdimzp87s0s1umw9AnFoVgHFFGz9UXRhIn9JEb9q1qOUbksBsART+PMBl0qlSmE8f0G95O9H6a5tYYjeD9iBIViCKTyogVajXbt+s1StVC+qrdvq/7xbJqrZfS1MYzjyV1cFvPKnmdAOcq6fcuuO+RcbTpTXEDYkmgAAAABJRU5ErkJggg==">
|
||||
</el-avatar>
|
||||
<h3>{{ displayNumber }}</h3>
|
||||
</div>
|
||||
<div class="popup-content">
|
||||
<div class="popup-info">
|
||||
<span>{{ model.province }}</span> <span>{{ model.city }}</span>
|
||||
</div>
|
||||
<time class="popup-timer text-muted"> {{ duration }}</time>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<el-button type="success" :disable="answering" v-if="needAnswer" size="large" circle title="接听">
|
||||
<icon name="phone" @click="handleAnswer"></icon>
|
||||
</el-button>
|
||||
<el-button type="warning" :disable="holding" v-if="inCalling" size="large" circle :title="holdLabel">
|
||||
<icon :name="isHold ? 'unmuted' : 'muted'" @click="handleToggleHold"></icon>
|
||||
</el-button>
|
||||
<el-button type="danger" :disable="hanguping" size="large" circle title="挂断">
|
||||
<icon name="phone" @click="handleHangup"></icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { hold, unhold, hangup } from '@/apis/control'
|
||||
import useUserStore from '@/stores/user'
|
||||
import { durationFormat } from '@/assets/js/formatter'
|
||||
import Icon from '@/components/widgets/Icon.vue';
|
||||
import Sip from '@/assets/js/sip'
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getUserAvatar, getUserInfo } from '@/apis/organize';
|
||||
|
||||
const props = defineProps({
|
||||
model: {
|
||||
type: Object,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const isHold = ref(false);
|
||||
|
||||
const duration = ref('');
|
||||
|
||||
const answering = ref(false);
|
||||
|
||||
const holding = ref(false);
|
||||
|
||||
const hanguping = ref(false);
|
||||
|
||||
const dragging = ref(false);
|
||||
|
||||
const avatar = ref('');
|
||||
|
||||
const position = ref({ left: 0, top: 0 });
|
||||
|
||||
const prevDragPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const needAnswer = computed(() => {
|
||||
if (!Sip.getIsRegistered()) {
|
||||
return false;
|
||||
}
|
||||
if (props.model.status !== 'setup') {
|
||||
return false;
|
||||
}
|
||||
if (props.model.direction === 'inbound') {
|
||||
return true;
|
||||
}
|
||||
const { uid } = useUserStore();
|
||||
if (props.model.direction === 'inner' && props.model.callee === uid) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
|
||||
const inCalling = computed(() => {
|
||||
return props.model.status === 'answered';
|
||||
})
|
||||
|
||||
const holdLabel = computed(() => {
|
||||
return isHold.value ? '接回' : '保持';
|
||||
})
|
||||
|
||||
const displayNumber = ref('');
|
||||
|
||||
const timerTicket = () => {
|
||||
if (props.model.modtime > 0) {
|
||||
duration.value = durationFormat(((new Date()).getTime() / 1000) - props.model.modtime, ':');
|
||||
}
|
||||
setTimeout(timerTicket, 1000);
|
||||
}
|
||||
|
||||
const handleClose = (e) => {
|
||||
avatar.value = '';
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
|
||||
|
||||
const handleAnswer = (e) => {
|
||||
answering.value = true;
|
||||
Sip.answer().then(res => {
|
||||
answering.value = false;
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
answering.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggleHold = (e) => {
|
||||
const { uid } = useUserStore();
|
||||
holding.value = true
|
||||
if (isHold.value) {
|
||||
unhold(uid).then(res => {
|
||||
holding.value = false
|
||||
isHold.value = false
|
||||
}).catch(e => {
|
||||
holding.value
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
} else {
|
||||
hold(uid).then(res => {
|
||||
holding.value
|
||||
isHold.value = true
|
||||
}).catch(e => {
|
||||
holding.value
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleHangup = (e) => {
|
||||
const { uid } = useUserStore()
|
||||
hanguping.value = true;
|
||||
if (props.model.status === 'setup' && props.model.direction === 'inbound' && Sip.getIsRegistered()) {
|
||||
Sip.decline().then(res => {
|
||||
hanguping.value = false
|
||||
}).catch(e => {
|
||||
hanguping.value = false
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
} else {
|
||||
if (Sip.getIsRegistered()) {
|
||||
Sip.hangup().then(res => {
|
||||
hanguping.value = false
|
||||
}).catch(e => {
|
||||
hanguping.value = false
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
} else {
|
||||
hangup(uid).then(res => {
|
||||
hanguping.value = false
|
||||
}).catch(e => {
|
||||
hanguping.value = false
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragStart = (e) => {
|
||||
dragging.value = true
|
||||
prevDragPosition.value.x = e.clientX
|
||||
prevDragPosition.value.y = e.clientY
|
||||
}
|
||||
|
||||
const handleDragging = (e) => {
|
||||
if (!dragging.value) {
|
||||
return
|
||||
}
|
||||
let x = e.clientX - prevDragPosition.value.x;
|
||||
let y = e.clientY - prevDragPosition.value.y;
|
||||
prevDragPosition.value.x = e.clientX;
|
||||
prevDragPosition.value.y = e.clientY;
|
||||
position.value.left += x;
|
||||
position.value.top += y;
|
||||
}
|
||||
|
||||
const handleDragged = (e) => {
|
||||
dragging.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
position.value.left = (window.innerWidth - 280) / 2
|
||||
position.value.top = window.innerHeight / 10
|
||||
document.addEventListener('mousemove', handleDragging)
|
||||
document.addEventListener('mouseup', handleDragged)
|
||||
const { uid } = useUserStore()
|
||||
|
||||
watch(() => { return props.model }, (val) => {
|
||||
if (val.direction === 'inbound') {
|
||||
displayNumber.value = val.caller;
|
||||
} else if (val.direction === 'outbound') {
|
||||
displayNumber.value = val.callee;
|
||||
} else if (val.direction === 'inner') {
|
||||
if (val.caller === uid) {
|
||||
displayNumber.value = val.callee;
|
||||
avatar.value = getUserAvatar(val.callee);
|
||||
} else {
|
||||
displayNumber.value = val.caller;
|
||||
avatar.value = getUserAvatar(val.caller);
|
||||
}
|
||||
getUserInfo(displayNumber.value).then(res => {
|
||||
displayNumber.value = `${res.username}(${res.id})`
|
||||
})
|
||||
}
|
||||
}, { deep: true })
|
||||
timerTicket();
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleDragging)
|
||||
document.removeEventListener('mouseup', handleDragged)
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,23 @@
|
|||
import ElementPlus from 'element-plus'
|
||||
import piniaPersistState from 'pinia-plugin-persistedstate'
|
||||
import 'normalize.css'
|
||||
import '@/assets/scss/element.scss'
|
||||
import '@/assets/scss/common.scss'
|
||||
import '@/assets/scss/style.scss'
|
||||
import permission from '@/directive/permission'
|
||||
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia().use(piniaPersistState))
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(permission)
|
||||
|
||||
app.mount('#app')
|
|
@ -0,0 +1,36 @@
|
|||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import LayoutView from '@/layouts/default/Layout.vue'
|
||||
import { getMenus, buildRoutes } from '@/assets/js/menu'
|
||||
|
||||
const components = import.meta.glob("../views/**/**.vue")
|
||||
|
||||
const childRoutes = buildRoutes(getMenus(), components);
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'App',
|
||||
component: LayoutView,
|
||||
children: childRoutes
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
meta: { allowGuest: true },
|
||||
component: () => import('../views/account/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/redirect/:path*',
|
||||
component: () => import('../views/redirect/Redirect.vue'),
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
export default router
|
|
@ -0,0 +1,12 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
|
@ -0,0 +1,67 @@
|
|||
import { defineStore } from 'pinia'
|
||||
|
||||
const useSystemStore = defineStore('system', {
|
||||
state: () => {
|
||||
return {
|
||||
loading: false,
|
||||
collapse: false,
|
||||
sidebarWidth: 200,
|
||||
flowSidebarVisible: false,
|
||||
lang: 'zh-CN',
|
||||
logoUrl: '//s3.tebi.io/tenos/images/logo/jc.png',
|
||||
copyright: '2005-2023 JUSTCALL 版权 © 2023 集时股份呼叫中心开发团队',
|
||||
productName: '呼叫中心平台',
|
||||
variables: {},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
actions: {
|
||||
activeLoading() {
|
||||
this.loading = true
|
||||
},
|
||||
inactiveLoading() {
|
||||
this.loading = false
|
||||
},
|
||||
setLanguage(lang) {
|
||||
this.lang = lang
|
||||
},
|
||||
toggleFlowSidebarVisible() {
|
||||
this.flowSidebarVisible = !this.flowSidebarVisible;
|
||||
if (this.flowSidebarVisible) {
|
||||
this.collapse = false;
|
||||
} else {
|
||||
this.sidebarWidth = 200;
|
||||
}
|
||||
},
|
||||
closeFlowSidebar() {
|
||||
if (this.flowSidebarVisible) {
|
||||
this.toggleFlowSidebarVisible();
|
||||
}
|
||||
},
|
||||
toggleCollapse() {
|
||||
this.collapse = !this.collapse;
|
||||
},
|
||||
setAttributeValue(attr, value) {
|
||||
switch (attr) {
|
||||
case 'name':
|
||||
this.productName = value;
|
||||
break
|
||||
case 'logo':
|
||||
this.logoUrl = value;
|
||||
break;
|
||||
case 'copyright':
|
||||
this.copyright = value;
|
||||
break;
|
||||
default:
|
||||
this.variables[attr] = value
|
||||
}
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
key: 'G_PTST4',
|
||||
paths: ['lang', 'collapse', 'sidebarWidth']
|
||||
},
|
||||
})
|
||||
|
||||
export default useSystemStore
|
|
@ -0,0 +1,18 @@
|
|||
import { defineStore } from 'pinia'
|
||||
|
||||
const useThemeStore = defineStore('theme', {
|
||||
state: () => {
|
||||
return {
|
||||
name: 'light',
|
||||
backgroundColor: '#F9FBFD',
|
||||
headerBackgroundColor: '#FFFFFF',
|
||||
sidebarBackgroundColor: '#152e4d',
|
||||
sidebarTextColor: '#a8b5c8',
|
||||
sidebarActiveTextColor: '#ffffff',
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
persist: true,
|
||||
})
|
||||
|
||||
export default useThemeStore
|
|
@ -0,0 +1,123 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { getBaseUrl } from '@/apis/request'
|
||||
|
||||
const useUserStore = defineStore('user', {
|
||||
state: () => {
|
||||
return {
|
||||
uid: '',
|
||||
username: '',
|
||||
email: '',
|
||||
avatar: '//rd.echo.me/fake/users/user-1.jpg',
|
||||
pwdhash: '',
|
||||
accessToken: '*',
|
||||
tokenExpiredAt: 0,
|
||||
permissions: [],
|
||||
description: '',
|
||||
loginDevice: false,
|
||||
hiddlePhone: false, //号码是否隐藏
|
||||
deviceRegistered: false,
|
||||
unreadMsgCount: 0,
|
||||
}
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
setUserID(uid) {
|
||||
this.uid = uid
|
||||
},
|
||||
setAccessToken(token, ttl) {
|
||||
this.accessToken = token
|
||||
if (ttl <= 0) {
|
||||
this.tokenExpiredAt = 0;
|
||||
sessionStorage.removeItem('G_PSID1');
|
||||
} else {
|
||||
this.tokenExpiredAt = ((new Date()).getTime() / 1000) + ttl;
|
||||
sessionStorage.setItem('G_PSID1', this.accessToken);
|
||||
}
|
||||
},
|
||||
setUsername(s) {
|
||||
this.username = s
|
||||
},
|
||||
|
||||
setEmail(s) {
|
||||
this.email = s
|
||||
},
|
||||
setPassword(s) {
|
||||
this.pwdhash = btoa(s)
|
||||
},
|
||||
setAvatar(s) {
|
||||
if (s.indexOf('//') === -1) {
|
||||
s = getBaseUrl() + s;
|
||||
}
|
||||
this.avatar = s
|
||||
},
|
||||
setAutoLoginDevice(s) {
|
||||
this.loginDevice = s;
|
||||
},
|
||||
setDeviceRegisterState(ok) {
|
||||
this.deviceRegistered = ok
|
||||
},
|
||||
setPermissions(s) {
|
||||
if (typeof s === 'string') {
|
||||
try {
|
||||
this.permissions = JSON.parse(s)
|
||||
} catch (e) { }
|
||||
} else {
|
||||
this.permissions = s
|
||||
}
|
||||
},
|
||||
isAuthorization() {
|
||||
let timestamp = (new Date()).getTime() / 1000;
|
||||
return this.tokenExpiredAt > timestamp;
|
||||
},
|
||||
incUnreadMsgCount() {
|
||||
this.unreadMsgCount++
|
||||
},
|
||||
decUnreadMsgCount() {
|
||||
this.unreadMsgCount--
|
||||
},
|
||||
setUnreadMsgCount(n) {
|
||||
this.unreadMsgCount = n
|
||||
},
|
||||
getUserID() {
|
||||
return this.uid;
|
||||
},
|
||||
getAccessToken() {
|
||||
return this.accessToken;
|
||||
},
|
||||
getAutoLoginDevice() {
|
||||
return this.loginDevice
|
||||
},
|
||||
getPassword() {
|
||||
return atob(this.pwdhash);
|
||||
},
|
||||
getUnreadMsgCount() {
|
||||
return this.unreadMsgCount
|
||||
},
|
||||
getDeviceRegistered() {
|
||||
return this.deviceRegistered;
|
||||
},
|
||||
hasPermission(permission) {
|
||||
let ret = false
|
||||
if (typeof permission === 'boolean') {
|
||||
ret = permission
|
||||
} else if (Array.isArray(permission)) {
|
||||
for (let s of permission) {
|
||||
if (this.permissions.indexOf(s) > -1) {
|
||||
ret = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = this.permissions.indexOf(permission) > -1
|
||||
}
|
||||
return ret
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
key: 'G_PTST3',
|
||||
paths: ['accessToken', 'tokenExpiredAt', 'pwdhash', 'loginDevice'],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
})
|
||||
|
||||
export default useUserStore
|
|
@ -0,0 +1,219 @@
|
|||
<template>
|
||||
<div class="login">
|
||||
<div class="login-container">
|
||||
<div class="text-center login-header">
|
||||
<h2>登录系统</h2>
|
||||
</div>
|
||||
<el-form label-position="top" size="large">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="model.username" placeholder="请输入用户名" :prefix-icon="User">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户密码" :error="errorMessage">
|
||||
<el-input v-model="model.password" placeholder="请输入用户密码" show-password :prefix-icon="Lock"
|
||||
@keyup="handleKeyUp">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="model.remember">记住密码</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<Vcode :show="verifyVisible" :sliderSize="18" :puzzleScale="0.6" className="v-puzzle-code"
|
||||
sliderText="" @success="onVerifySuccess" />
|
||||
<el-button type="primary" :loading="loading" :disable="loading" @click="handleLogin">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider>
|
||||
<span class="text-muted"></span>
|
||||
</el-divider>
|
||||
<div class="user-agreement">
|
||||
<el-checkbox v-model="agreement">
|
||||
<p class="text-muted text-small">同意 <a>用户协议</a> 和 <a>使用协议</a></p>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { userLogin, getUserProfile } from '@/apis/organize'
|
||||
import useUserStore from '@/stores/user'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { User, Lock } from '@element-plus/icons-vue';
|
||||
import Vcode from "vue3-puzzle-vcode";
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const model = ref({ username: '', password: '', remember: true })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const errorMessage = ref('')
|
||||
|
||||
const agreement = ref(true)
|
||||
|
||||
const verifyVisible = ref(false)
|
||||
|
||||
const submit = () => {
|
||||
errorMessage.value = '';
|
||||
if (!agreement.value) {
|
||||
ElMessage.error("请先同意用户协议和使用协议");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
userLogin(model.value.username, model.value.password).then(res => {
|
||||
const { setUserID, setAccessToken, setPassword, setUsername, setPermissions, setAvatar, setAutoLoginDevice } = useUserStore()
|
||||
setUserID(res.uid);
|
||||
setAccessToken(res.token, res.expire_in);
|
||||
setPassword(model.value.password);
|
||||
setAutoLoginDevice(model.value.remember);
|
||||
getUserProfile().then(res => {
|
||||
setUsername(res.username)
|
||||
setAvatar(res.avatar)
|
||||
setPermissions(res.permissions)
|
||||
loading.value = false
|
||||
router.push('/')
|
||||
}).catch(e => {
|
||||
loading.value = false
|
||||
})
|
||||
}).catch(e => {
|
||||
loading.value = false
|
||||
errorMessage.value = e.message
|
||||
})
|
||||
}
|
||||
|
||||
const onVerifySuccess = (e) => {
|
||||
if (verifyVisible.value) {
|
||||
verifyVisible.value = false;
|
||||
submit();
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
handleLogin()
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
let isDev = process.env.NODE_ENV !== 'production';
|
||||
if (!isDev) {
|
||||
verifyVisible.value = true;
|
||||
} else {
|
||||
submit();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
model.value = { username: '1000', password: 'Passwd#1000', remember: true };
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login {
|
||||
height: 100%;
|
||||
background-image: linear-gradient(160deg, #f5f5f9 0%, #e8ebf0 100%);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding-top: 10vh;
|
||||
|
||||
.login-container {
|
||||
width: 408px;
|
||||
max-width: 96%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
padding: 1.5rem 2rem;
|
||||
box-shadow: 0px 0px 30px 8px rgba(0, 0, 0, 0.046);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 1.1rem;
|
||||
margin-right: .9rem;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
letter-spacing: .28rem;
|
||||
color: rgba(50, 71, 92, 0.87);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user-agreement {
|
||||
a {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: .68rem;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
--el-input-border-radius: .375rem;
|
||||
--el-text-color-regular: #12263F;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
--el-border-radius-base: .375rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media(min-width: 576px) {
|
||||
|
||||
.container,
|
||||
.container-sm {
|
||||
max-width: 540px
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: 768px) {
|
||||
|
||||
.container,
|
||||
.container-md,
|
||||
.container-sm {
|
||||
max-width: 720px
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: 992px) {
|
||||
|
||||
.container,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm {
|
||||
max-width: 960px
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: 1200px) {
|
||||
|
||||
.container,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container-xl {
|
||||
max-width: 1140px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
|
||||
</script>
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<viewer :title="title" :module-name="moduleName" :table-name="tableName" :disable-toolbar="false"
|
||||
defaultSortable="id"></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 'moto'
|
||||
})
|
||||
|
||||
const tableName = computed(() => {
|
||||
return 'departments'
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
|
@ -0,0 +1,18 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue