Initial commit
This commit is contained in:
parent
219f2582bf
commit
f8882d4eea
|
@ -0,0 +1,5 @@
|
||||||
|
build/bin
|
||||||
|
node_modules
|
||||||
|
frontend/dist
|
||||||
|
.vscode
|
||||||
|
.idea
|
12
README.md
12
README.md
|
@ -1,2 +1,10 @@
|
||||||
# tiny-rdm
|
# README
|
||||||
Redis Desktop Manager
|
|
||||||
|
## Tiny RDM
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This is the official Wails Vue template.
|
||||||
|
|
||||||
|
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||||
|
here: https://wails.io/docs/reference/project-config
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App struct
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates a new App application struct
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup is called when the app starts. The context is saved
|
||||||
|
// so we can call the runtime methods
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greet returns a greeting for the given name
|
||||||
|
func (a *App) Greet(name string) string {
|
||||||
|
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||||
|
}
|
|
@ -0,0 +1,861 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
. "tinyrdm/backend/storage"
|
||||||
|
"tinyrdm/backend/types"
|
||||||
|
maputil "tinyrdm/backend/utils/map"
|
||||||
|
redis2 "tinyrdm/backend/utils/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type connectionService struct {
|
||||||
|
ctx context.Context
|
||||||
|
conns *ConnectionsStorage
|
||||||
|
connMap map[string]connectionItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectionItem struct {
|
||||||
|
rdb *redis.Client
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyItem struct {
|
||||||
|
Type string `json:"t"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var connection *connectionService
|
||||||
|
var onceConnection sync.Once
|
||||||
|
|
||||||
|
func Connection() *connectionService {
|
||||||
|
if connection == nil {
|
||||||
|
onceConnection.Do(func() {
|
||||||
|
connection = &connectionService{
|
||||||
|
conns: NewConnections(),
|
||||||
|
connMap: map[string]connectionItem{},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectionService) Start(ctx context.Context) {
|
||||||
|
c.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectionService) Stop(ctx context.Context) {
|
||||||
|
for _, item := range c.connMap {
|
||||||
|
if item.rdb != nil {
|
||||||
|
item.cancelFunc()
|
||||||
|
item.rdb.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.connMap = map[string]connectionItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *connectionService) TestConnection(host string, port int, username, password string) (resp types.JSResp) {
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", host, port),
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
defer rdb.Close()
|
||||||
|
if _, err := rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
} else {
|
||||||
|
resp.Success = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListConnection list all saved connection in local profile
|
||||||
|
func (c *connectionService) ListConnection() (resp types.JSResp) {
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = c.conns.GetConnections()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveConnection save connection config to local profile
|
||||||
|
func (c *connectionService) SaveConnection(param types.Connection, replace bool) (resp types.JSResp) {
|
||||||
|
if err := c.conns.UpsertConnection(param, replace); err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
} else {
|
||||||
|
resp.Success = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenConnection open redis server connection
|
||||||
|
func (c *connectionService) OpenConnection(name string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(name, 0)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get total database
|
||||||
|
config, err := rdb.ConfigGet(ctx, "databases").Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
totaldb, err := strconv.Atoi(config["database"])
|
||||||
|
if err != nil {
|
||||||
|
totaldb = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
// get database info
|
||||||
|
res, err := rdb.Info(ctx, "keyspace").Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = "list database fail:" + err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Parse all db, response content like below
|
||||||
|
var dbs []types.ConnectionDB
|
||||||
|
info := c.parseInfo(res)
|
||||||
|
for i := 0; i < totaldb; i++ {
|
||||||
|
dbName := "db" + strconv.Itoa(i)
|
||||||
|
dbInfoStr := info[dbName]
|
||||||
|
if len(dbInfoStr) > 0 {
|
||||||
|
dbInfo := c.parseDBItemInfo(dbInfoStr)
|
||||||
|
dbs = append(dbs, types.ConnectionDB{
|
||||||
|
Name: dbName,
|
||||||
|
Keys: dbInfo["keys"],
|
||||||
|
Expires: dbInfo["expires"],
|
||||||
|
AvgTTL: dbInfo["avg_ttl"],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dbs = append(dbs, types.ConnectionDB{
|
||||||
|
Name: dbName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"db": dbs,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseConnection close redis server connection
|
||||||
|
func (c *connectionService) CloseConnection(name string) (resp types.JSResp) {
|
||||||
|
item, ok := c.connMap[name]
|
||||||
|
if ok {
|
||||||
|
delete(c.connMap, name)
|
||||||
|
if item.rdb != nil {
|
||||||
|
item.cancelFunc()
|
||||||
|
item.rdb.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get redis client from local cache or create a new open
|
||||||
|
// if db >= 0, also switch to db index
|
||||||
|
func (c *connectionService) getRedisClient(connName string, db int) (*redis.Client, context.Context, error) {
|
||||||
|
item, ok := c.connMap[connName]
|
||||||
|
var rdb *redis.Client
|
||||||
|
var ctx context.Context
|
||||||
|
if ok {
|
||||||
|
rdb, ctx = item.rdb, item.ctx
|
||||||
|
} else {
|
||||||
|
connGroups := c.conns.GetConnections()
|
||||||
|
var selConn *types.Connection
|
||||||
|
for _, connGroup := range connGroups {
|
||||||
|
for _, conn := range connGroup.Connections {
|
||||||
|
if conn.Name == connName {
|
||||||
|
selConn = &conn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selConn == nil {
|
||||||
|
return nil, nil, errors.New("no match connection connName")
|
||||||
|
}
|
||||||
|
|
||||||
|
rdb = redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", selConn.Addr, selConn.Port),
|
||||||
|
Username: selConn.Username,
|
||||||
|
Password: selConn.Password,
|
||||||
|
ReadTimeout: -1,
|
||||||
|
})
|
||||||
|
rdb.AddHook(redis2.NewHook(connName))
|
||||||
|
|
||||||
|
if _, err := rdb.Ping(c.ctx).Result(); err != nil && err != redis.Nil {
|
||||||
|
return nil, nil, errors.New("can not connect to redis server:" + err.Error())
|
||||||
|
}
|
||||||
|
var cancelFunc context.CancelFunc
|
||||||
|
ctx, cancelFunc = context.WithCancel(c.ctx)
|
||||||
|
c.connMap[connName] = connectionItem{
|
||||||
|
rdb: rdb,
|
||||||
|
ctx: ctx,
|
||||||
|
cancelFunc: cancelFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if db >= 0 {
|
||||||
|
if err := rdb.Do(ctx, "SELECT", strconv.Itoa(db)).Err(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rdb, ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse command response content which use "redis info"
|
||||||
|
// # Keyspace\r\ndb0:keys=2,expires=1,avg_ttl=1877111749\r\ndb1:keys=33,expires=0,avg_ttl=0\r\ndb3:keys=17,expires=0,avg_ttl=0\r\ndb5:keys=3,expires=0,avg_ttl=0\r\n
|
||||||
|
func (c *connectionService) parseInfo(info string) map[string]string {
|
||||||
|
parsedInfo := map[string]string{}
|
||||||
|
lines := strings.Split(info, "\r\n")
|
||||||
|
if len(lines) > 0 {
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.HasPrefix(line, "#") {
|
||||||
|
items := strings.SplitN(line, ":", 2)
|
||||||
|
if len(items) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsedInfo[items[0]] = items[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse db item value, content format like below
|
||||||
|
// keys=2,expires=1,avg_ttl=1877111749
|
||||||
|
func (c *connectionService) parseDBItemInfo(info string) map[string]int {
|
||||||
|
ret := map[string]int{}
|
||||||
|
items := strings.Split(info, ",")
|
||||||
|
for _, item := range items {
|
||||||
|
kv := strings.SplitN(item, "=", 2)
|
||||||
|
if len(kv) > 1 {
|
||||||
|
ret[kv[0]], _ = strconv.Atoi(kv[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenDatabase open select database, and list all keys
|
||||||
|
// @param path contain connection name and db name
|
||||||
|
func (c *connectionService) OpenDatabase(connName string, db int) (resp types.JSResp) {
|
||||||
|
log.Println("open db:" + strconv.Itoa(db))
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//var keys []string
|
||||||
|
keys := map[string]keyItem{}
|
||||||
|
var cursor uint64
|
||||||
|
for {
|
||||||
|
var loadedKey []string
|
||||||
|
loadedKey, cursor, err = rdb.Scan(ctx, cursor, "*", 10000).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//c.updateDBKey(connName, db, loadedKey)
|
||||||
|
for _, k := range loadedKey {
|
||||||
|
//t, _ := rdb.Type(ctx, k).Result()
|
||||||
|
keys[k] = keyItem{Type: "t"}
|
||||||
|
}
|
||||||
|
//keys = append(keys, loadedKey...)
|
||||||
|
// no more loadedKey
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"keys": keys,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyValue get value by key
|
||||||
|
func (c *connectionService) GetKeyValue(connName string, db int, key string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyType string
|
||||||
|
var dur time.Duration
|
||||||
|
keyType, err = rdb.Type(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ttl int64
|
||||||
|
if dur, err = rdb.TTL(ctx, key).Result(); err != nil {
|
||||||
|
ttl = -1
|
||||||
|
} else {
|
||||||
|
if dur < 0 {
|
||||||
|
ttl = -1
|
||||||
|
} else {
|
||||||
|
ttl = int64(dur.Seconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value any
|
||||||
|
var cursor uint64
|
||||||
|
switch strings.ToLower(keyType) {
|
||||||
|
case "string":
|
||||||
|
value, err = rdb.Get(ctx, key).Result()
|
||||||
|
case "list":
|
||||||
|
value, err = rdb.LRange(ctx, key, 0, -1).Result()
|
||||||
|
case "hash":
|
||||||
|
//value, err = rdb.HGetAll(ctx, key).Result()
|
||||||
|
items := map[string]string{}
|
||||||
|
for {
|
||||||
|
var loadedVal []string
|
||||||
|
loadedVal, cursor, err = rdb.HScan(ctx, key, cursor, "*", 10000).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < len(loadedVal); i += 2 {
|
||||||
|
items[loadedVal[i]] = loadedVal[i+1]
|
||||||
|
}
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = items
|
||||||
|
case "set":
|
||||||
|
//value, err = rdb.SMembers(ctx, key).Result()
|
||||||
|
items := []string{}
|
||||||
|
for {
|
||||||
|
var loadedKey []string
|
||||||
|
loadedKey, cursor, err = rdb.SScan(ctx, key, cursor, "*", 10000).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items = append(items, loadedKey...)
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = items
|
||||||
|
case "zset":
|
||||||
|
//value, err = rdb.ZRangeWithScores(ctx, key, 0, -1).Result()
|
||||||
|
var items []types.ZSetItem
|
||||||
|
for {
|
||||||
|
var loadedVal []string
|
||||||
|
loadedVal, cursor, err = rdb.ZScan(ctx, key, cursor, "*", 10000).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var score float64
|
||||||
|
for i := 0; i < len(loadedVal); i += 2 {
|
||||||
|
if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
|
||||||
|
items = append(items, types.ZSetItem{
|
||||||
|
Value: loadedVal[i],
|
||||||
|
Score: score,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = items
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"type": keyType,
|
||||||
|
"ttl": ttl,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyValue set value by key
|
||||||
|
func (c *connectionService) SetKeyValue(connName string, db int, key, keyType string, value any, ttl int64) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiration time.Duration
|
||||||
|
if ttl < 0 {
|
||||||
|
expiration = redis.KeepTTL
|
||||||
|
} else {
|
||||||
|
expiration = time.Duration(ttl) * time.Second
|
||||||
|
}
|
||||||
|
switch strings.ToLower(keyType) {
|
||||||
|
case "string":
|
||||||
|
if str, ok := value.(string); !ok {
|
||||||
|
resp.Msg = "invalid string value"
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
_, err = rdb.Set(ctx, key, str, expiration).Result()
|
||||||
|
}
|
||||||
|
case "list":
|
||||||
|
if strs, ok := value.([]any); !ok {
|
||||||
|
resp.Msg = "invalid list value"
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||||
|
pipe.LPush(ctx, key, strs...)
|
||||||
|
if expiration > 0 {
|
||||||
|
pipe.Expire(ctx, key, expiration)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "hash":
|
||||||
|
if strs, ok := value.([]any); !ok {
|
||||||
|
resp.Msg = "invalid hash value"
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||||
|
if len(strs) > 1 {
|
||||||
|
for i := 0; i < len(strs); i += 2 {
|
||||||
|
pipe.HSetNX(ctx, key, strs[i].(string), strs[i+1])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pipe.HSet(ctx, key)
|
||||||
|
}
|
||||||
|
if expiration > 0 {
|
||||||
|
pipe.Expire(ctx, key, expiration)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "set":
|
||||||
|
if strs, ok := value.([]any); !ok || len(strs) <= 0 {
|
||||||
|
resp.Msg = "invalid set value"
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||||
|
if len(strs) > 1 {
|
||||||
|
for _, str := range strs {
|
||||||
|
pipe.SAdd(ctx, key, str.(string))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pipe.SAdd(ctx, key)
|
||||||
|
}
|
||||||
|
if expiration > 0 {
|
||||||
|
pipe.Expire(ctx, key, expiration)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "zset":
|
||||||
|
if strs, ok := value.([]any); !ok || len(strs) <= 0 {
|
||||||
|
resp.Msg = "invalid zset value"
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||||
|
var members []redis.Z
|
||||||
|
for i := 0; i < len(strs); i += 2 {
|
||||||
|
members = append(members, redis.Z{
|
||||||
|
Score: strs[i].(float64),
|
||||||
|
Member: strs[i+1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(members) > 0 {
|
||||||
|
pipe.ZAdd(ctx, key, members...)
|
||||||
|
} else {
|
||||||
|
pipe.ZAdd(ctx, key)
|
||||||
|
}
|
||||||
|
if expiration > 0 {
|
||||||
|
pipe.Expire(ctx, key, expiration)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHashValue set hash field
|
||||||
|
func (c *connectionService) SetHashValue(connName string, db int, key, field, newField, value string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var removedField []string
|
||||||
|
updatedField := map[string]string{}
|
||||||
|
if len(field) <= 0 {
|
||||||
|
// old filed is empty, add new field
|
||||||
|
_, err = rdb.HSet(ctx, key, newField, value).Result()
|
||||||
|
updatedField[newField] = value
|
||||||
|
} else if len(newField) <= 0 {
|
||||||
|
// new field is empty, delete old field
|
||||||
|
_, err = rdb.HDel(ctx, key, field, value).Result()
|
||||||
|
removedField = append(removedField, field)
|
||||||
|
} else if field == newField {
|
||||||
|
// replace field
|
||||||
|
_, err = rdb.HSet(ctx, key, newField, value).Result()
|
||||||
|
updatedField[newField] = value
|
||||||
|
} else {
|
||||||
|
// remove old field and add new field
|
||||||
|
if _, err = rdb.HDel(ctx, key, field).Result(); err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = rdb.HSet(ctx, key, newField, value).Result()
|
||||||
|
removedField = append(removedField, field)
|
||||||
|
updatedField[newField] = value
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"removed": removedField,
|
||||||
|
"updated": updatedField,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHashField add or update hash field
|
||||||
|
func (c *connectionService) AddHashField(connName string, db int, key string, action int, fieldItems []any) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := map[string]any{}
|
||||||
|
switch action {
|
||||||
|
case 1:
|
||||||
|
// ignore duplicated fields
|
||||||
|
for i := 0; i < len(fieldItems); i += 2 {
|
||||||
|
_, err = rdb.HSetNX(ctx, key, fieldItems[i].(string), fieldItems[i+1]).Result()
|
||||||
|
if err == nil {
|
||||||
|
updated[fieldItems[i].(string)] = fieldItems[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// overwrite duplicated fields
|
||||||
|
_, err = rdb.HSet(ctx, key, fieldItems...).Result()
|
||||||
|
for i := 0; i < len(fieldItems); i += 2 {
|
||||||
|
updated[fieldItems[i].(string)] = fieldItems[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"updated": updated,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddListItem add item to list or remove from it
|
||||||
|
func (c *connectionService) AddListItem(connName string, db int, key string, action int, items []any) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftPush, rightPush []any
|
||||||
|
switch action {
|
||||||
|
case 0:
|
||||||
|
// push to head
|
||||||
|
_, err = rdb.LPush(ctx, key, items...).Result()
|
||||||
|
leftPush = append(leftPush, items...)
|
||||||
|
default:
|
||||||
|
// append to tail
|
||||||
|
_, err = rdb.RPush(ctx, key, items...).Result()
|
||||||
|
rightPush = append(rightPush, items...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"left": leftPush,
|
||||||
|
"right": rightPush,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetListItem update or remove list item by index
|
||||||
|
func (c *connectionService) SetListItem(connName string, db int, key string, index int64, value string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed []int64
|
||||||
|
updated := map[int64]string{}
|
||||||
|
if len(value) <= 0 {
|
||||||
|
// remove from list
|
||||||
|
err = rdb.LSet(ctx, key, index, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rdb.LRem(ctx, key, 1, "---VALUE_REMOVED_BY_TINY_RDM---").Err()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removed = append(removed, index)
|
||||||
|
} else {
|
||||||
|
// replace index value
|
||||||
|
err = rdb.LSet(ctx, key, index, value).Err()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updated[index] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"removed": removed,
|
||||||
|
"updated": updated,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSetItem add members to set or remove from set
|
||||||
|
func (c *connectionService) SetSetItem(connName string, db int, key string, remove bool, members []any) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if remove {
|
||||||
|
_, err = rdb.SRem(ctx, key, members...).Result()
|
||||||
|
} else {
|
||||||
|
_, err = rdb.SAdd(ctx, key, members...).Result()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSetItem replace member of set
|
||||||
|
func (c *connectionService) UpdateSetItem(connName string, db int, key, value, newValue string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = rdb.SRem(ctx, key, value).Result()
|
||||||
|
_, err = rdb.SAdd(ctx, key, newValue).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateZSetValue update value of sorted set member
|
||||||
|
func (c *connectionService) UpdateZSetValue(connName string, db int, key, value, newValue string, score float64) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := map[string]any{}
|
||||||
|
var removed []string
|
||||||
|
if len(newValue) <= 0 {
|
||||||
|
// blank new value, delete value
|
||||||
|
_, err = rdb.ZRem(ctx, key, value).Result()
|
||||||
|
if err == nil {
|
||||||
|
removed = append(removed, value)
|
||||||
|
}
|
||||||
|
} else if newValue == value {
|
||||||
|
// update score only
|
||||||
|
_, err = rdb.ZAdd(ctx, key, redis.Z{
|
||||||
|
Score: score,
|
||||||
|
Member: value,
|
||||||
|
}).Result()
|
||||||
|
} else {
|
||||||
|
// remove old value and add new one
|
||||||
|
_, err = rdb.ZRem(ctx, key, value).Result()
|
||||||
|
if err == nil {
|
||||||
|
removed = append(removed, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rdb.ZAdd(ctx, key, redis.Z{
|
||||||
|
Score: score,
|
||||||
|
Member: newValue,
|
||||||
|
}).Result()
|
||||||
|
if err == nil {
|
||||||
|
updated[newValue] = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"updated": updated,
|
||||||
|
"removed": removed,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddZSetValue add item to sorted set
|
||||||
|
func (c *connectionService) AddZSetValue(connName string, db int, key string, action int, valueScore map[string]float64) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
members := maputil.ToSlice(valueScore, func(k string) redis.Z {
|
||||||
|
return redis.Z{
|
||||||
|
Score: valueScore[k],
|
||||||
|
Member: k,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case 1:
|
||||||
|
// ignore duplicated fields
|
||||||
|
_, err = rdb.ZAddNX(ctx, key, members...).Result()
|
||||||
|
default:
|
||||||
|
// overwrite duplicated fields
|
||||||
|
_, err = rdb.ZAdd(ctx, key, members...).Result()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyTTL set ttl of key
|
||||||
|
func (c *connectionService) SetKeyTTL(connName string, db int, key string, ttl int64) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiration time.Duration
|
||||||
|
if ttl < 0 {
|
||||||
|
if err = rdb.Persist(ctx, key).Err(); err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expiration = time.Duration(ttl) * time.Second
|
||||||
|
if err = rdb.Expire(ctx, key, expiration).Err(); err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveKey remove redis key
|
||||||
|
func (c *connectionService) RemoveKey(connName string, db int, key string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rmCount, err := rdb.Del(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
resp.Data = map[string]any{
|
||||||
|
"effect_count": rmCount,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameKey rename key
|
||||||
|
func (c *connectionService) RenameKey(connName string, db int, key, newKey string) (resp types.JSResp) {
|
||||||
|
rdb, ctx, err := c.getRedisClient(connName, db)
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rdb.RenameNX(ctx, key, newKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// update or insert key info to database
|
||||||
|
//func (c *connectionService) updateDBKey(connName string, db int, keys []string, separator string) {
|
||||||
|
// dbStruct := map[string]any{}
|
||||||
|
// for _, key := range keys {
|
||||||
|
// keyPart := strings.Split(key, separator)
|
||||||
|
// prefixLen := len(keyPart)-1
|
||||||
|
// if prefixLen > 0 {
|
||||||
|
// for i := 0; i < prefixLen; i++ {
|
||||||
|
// if dbStruct[keyPart[i]]
|
||||||
|
// keyPart[i]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// log.Println("key", key)
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type storageService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var storage *storageService
|
||||||
|
var onceStorage sync.Once
|
||||||
|
|
||||||
|
func Storage() *storageService {
|
||||||
|
if storage == nil {
|
||||||
|
onceStorage.Do(func() {
|
||||||
|
storage = &storageService{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return storage
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"sync"
|
||||||
|
"tinyrdm/backend/types"
|
||||||
|
sliceutil "tinyrdm/backend/utils/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnectionsStorage struct {
|
||||||
|
storage *localStorage
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnections() *ConnectionsStorage {
|
||||||
|
return &ConnectionsStorage{
|
||||||
|
storage: NewLocalStore("connections.yaml"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectionsStorage) defaultConnections() []types.ConnectionGroup {
|
||||||
|
return []types.ConnectionGroup{
|
||||||
|
{
|
||||||
|
GroupName: "",
|
||||||
|
Connections: []types.Connection{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectionsStorage) defaultConnectionItem() types.Connection {
|
||||||
|
return types.Connection{
|
||||||
|
Name: "",
|
||||||
|
Addr: "127.0.0.1",
|
||||||
|
Port: 6379,
|
||||||
|
Username: "",
|
||||||
|
Password: "",
|
||||||
|
DefaultFilter: "*",
|
||||||
|
KeySeparator: ":",
|
||||||
|
ConnTimeout: 60,
|
||||||
|
ExecTimeout: 60,
|
||||||
|
MarkColor: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectionsStorage) getConnections() (ret []types.ConnectionGroup) {
|
||||||
|
b, err := c.storage.Load()
|
||||||
|
if err != nil {
|
||||||
|
ret = c.defaultConnections()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = yaml.Unmarshal(b, &ret); err != nil {
|
||||||
|
ret = c.defaultConnections()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ret) <= 0 {
|
||||||
|
ret = c.defaultConnections()
|
||||||
|
}
|
||||||
|
//if !sliceutil.AnyMatch(ret, func(i int) bool {
|
||||||
|
// return ret[i].GroupName == ""
|
||||||
|
//}) {
|
||||||
|
// ret = append(ret, c.defaultConnections()...)
|
||||||
|
//}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnections get all store connections from local
|
||||||
|
func (c *ConnectionsStorage) GetConnections() (ret []types.ConnectionGroup) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
return c.getConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnectionsFlat get all store connections from local flat(exclude group level)
|
||||||
|
func (c *ConnectionsStorage) GetConnectionsFlat() (ret []types.Connection) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
conns := c.getConnections()
|
||||||
|
for _, group := range conns {
|
||||||
|
for _, conn := range group.Connections {
|
||||||
|
ret = append(ret, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectionsStorage) saveConnections(conns []types.ConnectionGroup) error {
|
||||||
|
b, err := yaml.Marshal(&conns)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = c.storage.Store(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertConnection update or insert a connection
|
||||||
|
func (c *ConnectionsStorage) UpsertConnection(param types.Connection, replace bool) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
conns := c.getConnections()
|
||||||
|
groupIndex := -1
|
||||||
|
connIndex := -1
|
||||||
|
for i, group := range conns {
|
||||||
|
for j, conn := range group.Connections {
|
||||||
|
// check conflict connection name
|
||||||
|
if conn.Name == param.Name {
|
||||||
|
if !replace {
|
||||||
|
return errors.New("duplicated connection name")
|
||||||
|
} else {
|
||||||
|
// different group name, should move group
|
||||||
|
// remove from current group first
|
||||||
|
if group.GroupName != param.Group {
|
||||||
|
group.Connections = append(group.Connections[:j], group.Connections[j+1:]...)
|
||||||
|
|
||||||
|
// find new group index
|
||||||
|
groupIndex, _ = sliceutil.Find(conns, func(i int) bool {
|
||||||
|
return conns[i].GroupName == param.Group
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
groupIndex = i
|
||||||
|
connIndex = j
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupIndex >= 0 {
|
||||||
|
// group exists
|
||||||
|
if connIndex >= 0 {
|
||||||
|
// connection exists
|
||||||
|
conns[groupIndex].Connections[connIndex] = param
|
||||||
|
} else {
|
||||||
|
// new connection
|
||||||
|
conns[groupIndex].Connections = append(conns[groupIndex].Connections, param)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// new group
|
||||||
|
group := types.ConnectionGroup{
|
||||||
|
GroupName: param.Group,
|
||||||
|
Connections: []types.Connection{param},
|
||||||
|
}
|
||||||
|
conns = append(conns, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.saveConnections(conns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveConnection remove special connection
|
||||||
|
func (c *ConnectionsStorage) RemoveConnection(group, name string) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
conns := c.getConnections()
|
||||||
|
for i, connGroup := range conns {
|
||||||
|
if connGroup.GroupName == group {
|
||||||
|
for j, conn := range connGroup.Connections {
|
||||||
|
if conn.Name == name {
|
||||||
|
connList := conns[i].Connections
|
||||||
|
connList = append(connList[:j], connList[j+1:]...)
|
||||||
|
conns[i].Connections = connList
|
||||||
|
return c.saveConnections(conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no match connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertGroup update or insert a group
|
||||||
|
// When want to create group only, set group == param.name
|
||||||
|
func (c *ConnectionsStorage) UpsertGroup(group string, param types.ConnectionGroup) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
conns := c.getConnections()
|
||||||
|
for i, connGroup := range conns {
|
||||||
|
if connGroup.GroupName == group {
|
||||||
|
conns[i].GroupName = param.GroupName
|
||||||
|
return c.saveConnections(conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match group, create one
|
||||||
|
connGroup := types.ConnectionGroup{
|
||||||
|
GroupName: param.GroupName,
|
||||||
|
Connections: []types.Connection{},
|
||||||
|
}
|
||||||
|
conns = append(conns, connGroup)
|
||||||
|
return c.saveConnections(conns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveGroup remove special group, include all connections under it
|
||||||
|
func (c *ConnectionsStorage) RemoveGroup(group string) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
conns := c.getConnections()
|
||||||
|
for i, connGroup := range conns {
|
||||||
|
if connGroup.GroupName == group {
|
||||||
|
conns = append(conns[:i], conns[i+1:]...)
|
||||||
|
return c.saveConnections(conns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no match group")
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vrischmann/userdir"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// localStorage provides reading and writing application data to the user's
|
||||||
|
// configuration directory.
|
||||||
|
type localStorage struct {
|
||||||
|
ConfPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocalStore returns a localStore instance.
|
||||||
|
func NewLocalStore(filename string) *localStorage {
|
||||||
|
return &localStorage{
|
||||||
|
ConfPath: path.Join(userdir.GetConfigHome(), "TinyRDM", filename),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads the given file in the user's configuration directory and returns
|
||||||
|
// its contents.
|
||||||
|
func (l *localStorage) Load() ([]byte, error) {
|
||||||
|
d, err := os.ReadFile(l.ConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store writes data to the user's configuration directory at the given
|
||||||
|
// filename.
|
||||||
|
func (l *localStorage) Store(data []byte) error {
|
||||||
|
dir := path.Dir(l.ConfPath)
|
||||||
|
if err := ensureDirExists(dir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(l.ConfPath, data, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDirExists checks for the existence of the directory at the given path,
|
||||||
|
// which is created if it does not exist.
|
||||||
|
func ensureDirExists(path string) error {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err = os.Mkdir(path, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PreferencesStorage struct {
|
||||||
|
storage *localStorage
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPreferences() *PreferencesStorage {
|
||||||
|
return &PreferencesStorage{
|
||||||
|
storage: NewLocalStore("preferences.yaml"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PreferencesStorage) DefaultPreferences() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"general": map[string]any{
|
||||||
|
"language": "en",
|
||||||
|
"font": "",
|
||||||
|
"font_size": 14,
|
||||||
|
"use_proxy": false,
|
||||||
|
"use_proxy_http": false,
|
||||||
|
"check_update": true,
|
||||||
|
},
|
||||||
|
"editor": map[string]any{
|
||||||
|
"font": "",
|
||||||
|
"font_size": 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PreferencesStorage) getPreferences() (ret map[string]any) {
|
||||||
|
b, err := p.storage.Load()
|
||||||
|
if err != nil {
|
||||||
|
ret = p.DefaultPreferences()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(b, &ret); err != nil {
|
||||||
|
ret = p.DefaultPreferences()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreferences Get preferences from local
|
||||||
|
func (p *PreferencesStorage) GetPreferences() (ret map[string]any) {
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
|
return p.getPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PreferencesStorage) setPreferences(pf map[string]any, key string, value any) error {
|
||||||
|
keyPath := strings.Split(key, ".")
|
||||||
|
if len(keyPath) <= 0 {
|
||||||
|
return fmt.Errorf("invalid key path(%s)", key)
|
||||||
|
}
|
||||||
|
var node any = pf
|
||||||
|
for _, k := range keyPath[:len(keyPath)-1] {
|
||||||
|
if subNode, ok := node.(map[string]any); ok {
|
||||||
|
node = subNode[k]
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("invalid key path(%s)", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if subNode, ok := node.(map[string]any); ok {
|
||||||
|
subNode[keyPath[len(keyPath)-1]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PreferencesStorage) savePreferences(pf map[string]any) error {
|
||||||
|
b, err := yaml.Marshal(&pf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.storage.Store(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPreferences assign value to key path, the key path use "." to indicate multiple level
|
||||||
|
func (p *PreferencesStorage) SetPreferences(key string, value any) error {
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
|
pf := p.getPreferences()
|
||||||
|
if err := p.setPreferences(pf, key, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.savePreferences(pf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPreferencesN set multiple key path and value
|
||||||
|
func (p *PreferencesStorage) SetPreferencesN(values map[string]any) error {
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
|
||||||
|
pf := p.getPreferences()
|
||||||
|
for path, v := range values {
|
||||||
|
if err := p.setPreferences(pf, path, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.savePreferences(pf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PreferencesStorage) RestoreDefault() map[string]any {
|
||||||
|
pf := p.DefaultPreferences()
|
||||||
|
p.savePreferences(pf)
|
||||||
|
return pf
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type ConnectionCategory int
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
Group string `json:"group" yaml:"-"`
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
Addr string `json:"addr" yaml:"addr"`
|
||||||
|
Port int `json:"port" yaml:"port"`
|
||||||
|
Username string `json:"username" yaml:"username"`
|
||||||
|
Password string `json:"password" yaml:"password"`
|
||||||
|
DefaultFilter string `json:"defaultFilter" yaml:"default_filter"`
|
||||||
|
KeySeparator string `json:"keySeparator" yaml:"key_separator"`
|
||||||
|
ConnTimeout int `json:"connTimeout" yaml:"conn_timeout"`
|
||||||
|
ExecTimeout int `json:"execTimeout" yaml:"exec_timeout"`
|
||||||
|
MarkColor string `json:"markColor" yaml:"mark_color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionGroup struct {
|
||||||
|
GroupName string `json:"groupName" yaml:"group_name"`
|
||||||
|
Connections []Connection `json:"connections" yaml:"connections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionDB struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Keys int `json:"keys"`
|
||||||
|
Expires int `json:"expires,omitempty"`
|
||||||
|
AvgTTL int `json:"avgTtl,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type JSResp struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type ZSetItem struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package coll
|
||||||
|
|
||||||
|
// Queue 队列, 先进先出
|
||||||
|
type Queue[T any] []T
|
||||||
|
|
||||||
|
func NewQueue[T any](elems ...T) Queue[T] {
|
||||||
|
if len(elems) > 0 {
|
||||||
|
data := make([]T, len(elems))
|
||||||
|
copy(data, elems)
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return Queue[T]{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push 尾部插入元素
|
||||||
|
func (q *Queue[T]) Push(elem T) {
|
||||||
|
if q == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*q = append(*q, elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushN 尾部插入多个元素
|
||||||
|
func (q *Queue[T]) PushN(elems ...T) {
|
||||||
|
if q == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(elems) <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*q = append(*q, elems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop 移除并返回头部元素
|
||||||
|
func (q *Queue[T]) Pop() (T, bool) {
|
||||||
|
var elem T
|
||||||
|
if q == nil || len(*q) <= 0 {
|
||||||
|
return elem, false
|
||||||
|
}
|
||||||
|
elem = (*q)[0]
|
||||||
|
*q = (*q)[1:]
|
||||||
|
return elem, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue[T]) PopN(n int) []T {
|
||||||
|
if q == nil {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var popElems []T
|
||||||
|
if n <= 0 {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
l := len(*q)
|
||||||
|
if n >= l {
|
||||||
|
popElems = *q
|
||||||
|
*q = []T{}
|
||||||
|
return *q
|
||||||
|
}
|
||||||
|
|
||||||
|
popElems = (*q)[:n]
|
||||||
|
*q = (*q)[n:]
|
||||||
|
return popElems
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 移除所有元素
|
||||||
|
func (q *Queue[T]) Clear() {
|
||||||
|
if q == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*q = []T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue[T]) IsEmpty() bool {
|
||||||
|
return len(q) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue[T]) Size() int {
|
||||||
|
return len(q)
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
package coll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
json "github.com/bytedance/sonic"
|
||||||
|
"sort"
|
||||||
|
. "tinyrdm/backend/utils"
|
||||||
|
"tinyrdm/backend/utils/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Void struct{}
|
||||||
|
|
||||||
|
// Set 集合, 存放不重复的元素
|
||||||
|
type Set[T Hashable] map[T]Void
|
||||||
|
|
||||||
|
// type Set[T Hashable] struct {
|
||||||
|
// data map[T]Void
|
||||||
|
// }
|
||||||
|
|
||||||
|
func NewSet[T Hashable](elems ...T) Set[T] {
|
||||||
|
if len(elems) > 0 {
|
||||||
|
data := make(Set[T], len(elems))
|
||||||
|
for _, e := range elems {
|
||||||
|
data[e] = Void{}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return Set[T]{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 添加元素
|
||||||
|
func (s Set[T]) Add(elem T) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, exists := s[elem]; !exists {
|
||||||
|
s[elem] = Void{}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddN 添加多个元素
|
||||||
|
func (s Set[T]) AddN(elems ...T) int {
|
||||||
|
if s == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
addCount := 0
|
||||||
|
var exists bool
|
||||||
|
for _, elem := range elems {
|
||||||
|
if _, exists = s[elem]; !exists {
|
||||||
|
s[elem] = Void{}
|
||||||
|
addCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge 合并其他集合
|
||||||
|
func (s Set[T]) Merge(other Set[T]) int {
|
||||||
|
return s.AddN(other.ToSlice()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains 判断是否存在指定元素
|
||||||
|
func (s Set[T]) Contains(elem T) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, exists := s[elem]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainAny 判断是否包含任意元素
|
||||||
|
func (s Set[T]) ContainAny(elems ...T) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var exists bool
|
||||||
|
for _, elem := range elems {
|
||||||
|
if _, exists = s[elem]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals 判断两个集合内元素是否一致
|
||||||
|
func (s Set[T]) Equals(other Set[T]) bool {
|
||||||
|
if s.Size() != other.Size() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for elem := range s {
|
||||||
|
if !other.Contains(elem) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainAll 判断是否包含所有元素
|
||||||
|
func (s Set[T]) ContainAll(elems ...T) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var exists bool
|
||||||
|
for _, elem := range elems {
|
||||||
|
if _, exists = s[elem]; !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 移除元素
|
||||||
|
func (s Set[T]) Remove(elem T) bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, exists := s[elem]; exists {
|
||||||
|
delete(s, elem)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveN 移除多个元素
|
||||||
|
func (s Set[T]) RemoveN(elems ...T) int {
|
||||||
|
if s == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var exists bool
|
||||||
|
removeCnt := 0
|
||||||
|
for _, elem := range elems {
|
||||||
|
if _, exists = s[elem]; exists {
|
||||||
|
delete(s, elem)
|
||||||
|
removeCnt += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removeCnt
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveSub 移除子集
|
||||||
|
func (s Set[T]) RemoveSub(subSet Set[T]) int {
|
||||||
|
if s == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var exists bool
|
||||||
|
removeCnt := 0
|
||||||
|
for elem := range subSet {
|
||||||
|
if _, exists = s[elem]; exists {
|
||||||
|
delete(s, elem)
|
||||||
|
removeCnt += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removeCnt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter 根据条件筛出符合的元素
|
||||||
|
func (s Set[T]) Filter(filterFunc func(i T) bool) []T {
|
||||||
|
ret := []T{}
|
||||||
|
for v := range s {
|
||||||
|
if filterFunc(v) {
|
||||||
|
ret = append(ret, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomElem 随机抽取一个元素
|
||||||
|
// @param remove 随机出来的元素是否同时从集合中移除
|
||||||
|
// @return 抽取的元素
|
||||||
|
// @return 是否抽取成功
|
||||||
|
func (s Set[T]) RandomElem(remove bool) (T, bool) {
|
||||||
|
size := s.Size()
|
||||||
|
if size > 0 {
|
||||||
|
selIdx := rand.Intn(size)
|
||||||
|
idx := 0
|
||||||
|
for elem := range s {
|
||||||
|
if idx == selIdx {
|
||||||
|
if remove {
|
||||||
|
delete(s, elem)
|
||||||
|
}
|
||||||
|
return elem, true
|
||||||
|
} else {
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r T
|
||||||
|
return r, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size 集合长度
|
||||||
|
func (s Set[T]) Size() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty 判断是否为空
|
||||||
|
func (s Set[T]) IsEmpty() bool {
|
||||||
|
return len(s) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空集合
|
||||||
|
func (s Set[T]) Clear() {
|
||||||
|
for elem := range s {
|
||||||
|
delete(s, elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSlice 转为切片
|
||||||
|
func (s Set[T]) ToSlice() []T {
|
||||||
|
size := len(s)
|
||||||
|
if size <= 0 {
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]T, 0, size)
|
||||||
|
for elem := range s {
|
||||||
|
ret = append(ret, elem)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSortedSlice 转为排序好的切片
|
||||||
|
func (s Set[T]) ToSortedSlice(sortFunc func(v1, v2 T) bool) []T {
|
||||||
|
list := s.ToSlice()
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
return sortFunc(list[i], list[j])
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each 遍历检索每个元素
|
||||||
|
func (s Set[T]) Each(eachFunc func(T)) {
|
||||||
|
if len(s) <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for elem := range s {
|
||||||
|
eachFunc(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone 克隆
|
||||||
|
func (s Set[T]) Clone() Set[T] {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
other := NewSet[T]()
|
||||||
|
for elem := range s {
|
||||||
|
other[elem] = Void{}
|
||||||
|
}
|
||||||
|
return other
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) String() string {
|
||||||
|
arr := s.ToSlice()
|
||||||
|
return fmt.Sprintf("%v", arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON to output non base64 encoded []byte
|
||||||
|
func (s Set[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
if s == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
t := s.ToSlice()
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON to deserialize []byte
|
||||||
|
func (s *Set[T]) UnmarshalJSON(b []byte) error {
|
||||||
|
t := []T{}
|
||||||
|
err := json.Unmarshal(b, &t)
|
||||||
|
if err != nil {
|
||||||
|
*s = NewSet[T]()
|
||||||
|
} else {
|
||||||
|
*s = NewSet[T](t...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GormDataType gorm common data type
|
||||||
|
func (s Set[T]) GormDataType() string {
|
||||||
|
return "json"
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package coll
|
||||||
|
|
||||||
|
// Stack 栈, 先进后出
|
||||||
|
type Stack[T any] []T
|
||||||
|
|
||||||
|
func NewStack[T any](elems ...T) Stack[T] {
|
||||||
|
if len(elems) > 0 {
|
||||||
|
data := make([]T, len(elems))
|
||||||
|
copy(data, elems)
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
return Stack[T]{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push 顶部添加一个元素
|
||||||
|
func (s *Stack[T]) Push(elem T) {
|
||||||
|
if s == nil {
|
||||||
|
panic("queue should not be nil")
|
||||||
|
}
|
||||||
|
*s = append(*s, elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushN 顶部添加一个元素
|
||||||
|
func (s *Stack[T]) PushN(elems ...T) {
|
||||||
|
if s == nil {
|
||||||
|
panic("queue should not be nil")
|
||||||
|
}
|
||||||
|
if len(elems) <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*s = append(*s, elems...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop 移除并返回顶部元素
|
||||||
|
func (s *Stack[T]) Pop() T {
|
||||||
|
if s == nil {
|
||||||
|
panic("queue should not be nil")
|
||||||
|
}
|
||||||
|
l := len(*s)
|
||||||
|
popElem := (*s)[l-1]
|
||||||
|
*s = (*s)[:l-1]
|
||||||
|
return popElem
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopN 移除并返回顶部多个元素
|
||||||
|
func (s *Stack[T]) PopN(n int) []T {
|
||||||
|
if s == nil {
|
||||||
|
panic("queue should not be nil")
|
||||||
|
}
|
||||||
|
var popElems []T
|
||||||
|
if n <= 0 {
|
||||||
|
return popElems
|
||||||
|
}
|
||||||
|
|
||||||
|
l := len(*s)
|
||||||
|
if n >= l {
|
||||||
|
popElems = *s
|
||||||
|
*s = []T{}
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
|
||||||
|
popElems = (*s)[l-n:]
|
||||||
|
*s = (*s)[:l-n]
|
||||||
|
|
||||||
|
// 翻转弹出结果
|
||||||
|
pl := len(popElems)
|
||||||
|
for i := 0; i < pl/2; i++ {
|
||||||
|
popElems[i], popElems[pl-i-1] = popElems[pl-i-1], popElems[i]
|
||||||
|
}
|
||||||
|
return popElems
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 移除所有元素
|
||||||
|
func (s *Stack[T]) Clear() {
|
||||||
|
if s == nil {
|
||||||
|
panic("queue should not be nil")
|
||||||
|
}
|
||||||
|
*s = []T{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Stack[T]) IsEmpty() bool {
|
||||||
|
return len(s) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Stack[T]) Size() int {
|
||||||
|
return len(s)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
type Hashable interface {
|
||||||
|
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignedNumber interface {
|
||||||
|
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnsignedNumber interface {
|
||||||
|
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
|
||||||
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
package maputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "tinyrdm/backend/utils"
|
||||||
|
"tinyrdm/backend/utils/coll"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get 获取键值对指定键的值, 如果不存在则返回自定默认值
|
||||||
|
func Get[M ~map[K]V, K Hashable, V any](m M, key K, defaultVal V) V {
|
||||||
|
if m != nil {
|
||||||
|
if v, exists := m[key]; exists {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsKey 判断指定键是否存在
|
||||||
|
func ContainsKey[M ~map[K]V, K Hashable, V any](m M, key K) bool {
|
||||||
|
if m == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, exists := m[key]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGet 获取键值对指定键的值, 如果不存在则调用给定的函数进行获取
|
||||||
|
func MustGet[M ~map[K]V, K Hashable, V any](m M, key K, getFunc func(K) V) V {
|
||||||
|
if v, exists := m[key]; exists {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if getFunc != nil {
|
||||||
|
return getFunc(key)
|
||||||
|
}
|
||||||
|
var defaultV V
|
||||||
|
return defaultV
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys 获取键值对中所有键
|
||||||
|
func Keys[M ~map[K]V, K Hashable, V any](m M) []K {
|
||||||
|
if len(m) <= 0 {
|
||||||
|
return []K{}
|
||||||
|
}
|
||||||
|
keys := make([]K, len(m))
|
||||||
|
index := 0
|
||||||
|
for k := range m {
|
||||||
|
keys[index] = k
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeySet 获取键值对中所有键集合
|
||||||
|
func KeySet[M ~map[K]V, K Hashable, V any](m M) coll.Set[K] {
|
||||||
|
if len(m) <= 0 {
|
||||||
|
return coll.NewSet[K]()
|
||||||
|
}
|
||||||
|
keySet := coll.NewSet[K]()
|
||||||
|
for k := range m {
|
||||||
|
keySet.Add(k)
|
||||||
|
}
|
||||||
|
return keySet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values 获取键值对中所有值
|
||||||
|
func Values[M ~map[K]V, K Hashable, V any](m M) []V {
|
||||||
|
if len(m) <= 0 {
|
||||||
|
return []V{}
|
||||||
|
}
|
||||||
|
values := make([]V, len(m))
|
||||||
|
index := 0
|
||||||
|
for _, v := range m {
|
||||||
|
values[index] = v
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueSet 获取键值对中所有值集合
|
||||||
|
func ValueSet[M ~map[K]V, K Hashable, V Hashable](m M) coll.Set[V] {
|
||||||
|
if len(m) <= 0 {
|
||||||
|
return coll.NewSet[V]()
|
||||||
|
}
|
||||||
|
valueSet := coll.NewSet[V]()
|
||||||
|
for _, v := range m {
|
||||||
|
valueSet.Add(v)
|
||||||
|
}
|
||||||
|
return valueSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill 填充键值对
|
||||||
|
func Fill[M ~map[K]V, K Hashable, V any](dest M, src M) M {
|
||||||
|
for k, v := range src {
|
||||||
|
dest[k] = v
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge 合并键值对, 后续键值对有重复键的元素会覆盖旧元素
|
||||||
|
func Merge[M ~map[K]V, K Hashable, V any](mapArr ...M) M {
|
||||||
|
result := make(M, len(mapArr))
|
||||||
|
for _, m := range mapArr {
|
||||||
|
for k, v := range m {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepMerge 深度递归覆盖src值到dst中
|
||||||
|
// 将返回新的值
|
||||||
|
func DeepMerge[M ~map[K]any, K Hashable](src1, src2 M) M {
|
||||||
|
out := make(map[K]any, len(src1))
|
||||||
|
for k, v := range src1 {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range src2 {
|
||||||
|
if v1, ok := v.(map[K]any); ok {
|
||||||
|
if bv, ok := out[k]; ok {
|
||||||
|
if bv1, ok := bv.(map[K]any); ok {
|
||||||
|
out[k] = DeepMerge(bv1, v1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omit 根据条件省略指定元素
|
||||||
|
func Omit[M ~map[K]V, K Hashable, V any](m M, omitFunc func(k K, v V) bool) (M, []K) {
|
||||||
|
result := M{}
|
||||||
|
var removedKeys []K
|
||||||
|
for k, v := range m {
|
||||||
|
if !omitFunc(k, v) {
|
||||||
|
result[k] = v
|
||||||
|
} else {
|
||||||
|
removedKeys = append(removedKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, removedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
// OmitKeys 省略指定键的的元素
|
||||||
|
func OmitKeys[M ~map[K]V, K Hashable, V any](m M, keys ...K) M {
|
||||||
|
omitKey := map[K]struct{}{}
|
||||||
|
for _, k := range keys {
|
||||||
|
omitKey[k] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := M{}
|
||||||
|
var exists bool
|
||||||
|
for k, v := range m {
|
||||||
|
if _, exists = omitKey[k]; !exists {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsAnyKey 是否包含任意键
|
||||||
|
func ContainsAnyKey[M ~map[K]V, K Hashable, V any](m M, keys ...K) bool {
|
||||||
|
var exists bool
|
||||||
|
for _, key := range keys {
|
||||||
|
if _, exists = m[key]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsAllKey 是否包含所有键
|
||||||
|
func ContainsAllKey[M ~map[K]V, K Hashable, V any](m M, keys ...K) bool {
|
||||||
|
var exists bool
|
||||||
|
for _, key := range keys {
|
||||||
|
if _, exists = m[key]; !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyMatch 是否任意元素符合条件
|
||||||
|
func AnyMatch[M ~map[K]V, K Hashable, V any](m M, matchFunc func(k K, v V) bool) bool {
|
||||||
|
for k, v := range m {
|
||||||
|
if matchFunc(k, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllMatch 是否所有元素符合条件
|
||||||
|
func AllMatch[M ~map[K]V, K Hashable, V any](m M, matchFunc func(k K, v V) bool) bool {
|
||||||
|
for k, v := range m {
|
||||||
|
if !matchFunc(k, v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce 累计
|
||||||
|
func Reduce[M ~map[K]V, K Hashable, V any, R any](m M, init R, reduceFunc func(R, K, V) R) R {
|
||||||
|
result := init
|
||||||
|
for k, v := range m {
|
||||||
|
result = reduceFunc(result, k, v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSlice 键值对转切片
|
||||||
|
func ToSlice[M ~map[K]V, K Hashable, V any, R any](m M, mapFunc func(k K) R) []R {
|
||||||
|
ret := make([]R, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
ret = append(ret, mapFunc(k))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter 筛选出指定条件的所有元素
|
||||||
|
func Filter[M ~map[K]V, K Hashable, V any](m M, filterFunc func(k K) bool) M {
|
||||||
|
ret := make(M, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
if filterFunc(k) {
|
||||||
|
ret[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterToSlice 键值对筛选并转切片
|
||||||
|
func FilterToSlice[M ~map[K]V, K Hashable, V any, R any](m M, mapFunc func(k K) (R, bool)) []R {
|
||||||
|
ret := make([]R, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
if v, filter := mapFunc(k); filter {
|
||||||
|
ret = append(ret, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterKey 筛选出指定条件的所有键
|
||||||
|
func FilterKey[M ~map[K]V, K Hashable, V any](m M, filterFunc func(k K) bool) []K {
|
||||||
|
ret := make([]K, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
if filterFunc(k) {
|
||||||
|
ret = append(ret, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone 复制键值对
|
||||||
|
func Clone[M ~map[K]V, K Hashable, V any](src M) M {
|
||||||
|
dest := make(M, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
dest[k] = v
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse 键->值映射翻转为值->键映射(如果重复则覆盖最后的)
|
||||||
|
func Reverse[M ~map[K]V, K Hashable, V Hashable](src M) map[V]K {
|
||||||
|
dest := make(map[V]K, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
dest[v] = k
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseAll 键->值映射翻转为值->键列表映射
|
||||||
|
func ReverseAll[M ~map[K]V, K Hashable, V Hashable](src M) map[V][]K {
|
||||||
|
dest := make(map[V][]K, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
dest[v] = append(dest[v], k)
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveIf 移除指定条件的键
|
||||||
|
func RemoveIf[M ~map[K]V, K Hashable, V any](src M, cond func(key K) bool) {
|
||||||
|
for k := range src {
|
||||||
|
if cond(k) {
|
||||||
|
delete(src, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package rand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 随机对象缓存池(解决自带随机函数全局抢锁问题)
|
||||||
|
var randObjectPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var lowerChar = []rune("abcdefghijklmnopqrstuvwxyz") // strings.Split("abcdefghijklmnopqrstuvwxyz", "")
|
||||||
|
var upperChar = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") // strings.Split("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "")
|
||||||
|
var numberChar = []rune("0123456789") // strings.Split("0123456789", "")
|
||||||
|
var numberAndChar = append(lowerChar, numberChar...)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Intn[T ~int](n T) T {
|
||||||
|
r := randObjectPool.Get()
|
||||||
|
res := r.(*rand.Rand).Intn(int(n))
|
||||||
|
randObjectPool.Put(r)
|
||||||
|
return T(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntnCount[T ~int](n T, count int) []T {
|
||||||
|
res := make([]T, count)
|
||||||
|
r := randObjectPool.Get()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
res[i] = T(r.(*rand.Rand).Intn(int(n)))
|
||||||
|
}
|
||||||
|
randObjectPool.Put(r)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int31n 生成<n的32位整形
|
||||||
|
func Int31n[T ~int32](n T) T {
|
||||||
|
r := randObjectPool.Get()
|
||||||
|
res := r.(*rand.Rand).Int31n(int32(n))
|
||||||
|
randObjectPool.Put(r)
|
||||||
|
return T(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int63n 生成小于n的64位整形
|
||||||
|
func Int63n[T ~int64](n T) T {
|
||||||
|
r := randObjectPool.Get()
|
||||||
|
res := r.(*rand.Rand).Int63n(int64(n))
|
||||||
|
randObjectPool.Put(r)
|
||||||
|
return T(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeInt 获取范围内的随机整数[min, max)
|
||||||
|
func RangeInt[T ~int](min, max T) T {
|
||||||
|
if min > max {
|
||||||
|
min, max = max, min
|
||||||
|
}
|
||||||
|
return Intn(max-min) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeString 生成随机字符串
|
||||||
|
func RangeString(charSet []rune, n int) string {
|
||||||
|
r := randObjectPool.Get()
|
||||||
|
|
||||||
|
res := strings.Builder{}
|
||||||
|
size := len(charSet)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
res.WriteRune(charSet[r.(*rand.Rand).Intn(size)])
|
||||||
|
}
|
||||||
|
randObjectPool.Put(r)
|
||||||
|
return res.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LowerString 生成随机指定长度小写字母
|
||||||
|
func LowerString(n int) string {
|
||||||
|
return RangeString(lowerChar, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpperString 生成随机指定长度大写字母
|
||||||
|
func UpperString(n int) string {
|
||||||
|
return RangeString(upperChar, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumberString 生成随机指定长度数字字符串
|
||||||
|
func NumberString(n int) string {
|
||||||
|
return RangeString(numberChar, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharNumberString 生成随机指定长度小写字母和数字
|
||||||
|
func CharNumberString(n int) string {
|
||||||
|
return RangeString(numberAndChar, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle 执行指定次数打乱
|
||||||
|
func Shuffle(n int, swap func(i, j int)) {
|
||||||
|
r := randObjectPool.Get()
|
||||||
|
r.(*rand.Rand).Shuffle(n, swap)
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package rand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WeightObject 权重单项
|
||||||
|
type WeightObject[T any] struct {
|
||||||
|
Obj T
|
||||||
|
Weight int
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeightRandom 根据权重随机
|
||||||
|
type WeightRandom[T any] struct {
|
||||||
|
WeightObject []WeightObject[T]
|
||||||
|
totalWeight int
|
||||||
|
randObj *rand.Rand
|
||||||
|
lk sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeightRandom[T any]() *WeightRandom[T] {
|
||||||
|
return &WeightRandom[T]{
|
||||||
|
WeightObject: []WeightObject[T]{},
|
||||||
|
totalWeight: 0,
|
||||||
|
randObj: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeightRandom[T]) Add(object T, weight int) {
|
||||||
|
weightObj := WeightObject[T]{
|
||||||
|
Obj: object,
|
||||||
|
Weight: weight,
|
||||||
|
}
|
||||||
|
w.AddObject(weightObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddObject 添加单个权重对象
|
||||||
|
func (w *WeightRandom[T]) AddObject(weightObject WeightObject[T]) {
|
||||||
|
if weightObject.Weight <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exists := false
|
||||||
|
for i, object := range w.WeightObject {
|
||||||
|
if cmp.Equal(weightObject.Obj, object.Obj) {
|
||||||
|
// 已经存在, 覆盖权重
|
||||||
|
w.subWeight(object.Weight)
|
||||||
|
w.WeightObject[i].Weight = weightObject.Weight
|
||||||
|
w.addWeight(weightObject.Weight)
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
// 已经存在, 覆盖权重
|
||||||
|
w.WeightObject = append(w.WeightObject, weightObject)
|
||||||
|
w.addWeight(weightObject.Weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddObjects 添加多个权重对象
|
||||||
|
func (w *WeightRandom[T]) AddObjects(object []WeightObject[T]) {
|
||||||
|
for _, weightObject := range object {
|
||||||
|
w.AddObject(weightObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeightRandom[T]) addWeight(weight int) {
|
||||||
|
if w.totalWeight < 0 {
|
||||||
|
w.totalWeight = 0
|
||||||
|
}
|
||||||
|
w.totalWeight += weight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WeightRandom[T]) subWeight(weight int) {
|
||||||
|
if w.totalWeight-weight < 0 {
|
||||||
|
w.totalWeight = 0
|
||||||
|
} else {
|
||||||
|
w.totalWeight -= weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next 通过权重随机到下一个
|
||||||
|
func (w *WeightRandom[T]) Next() T {
|
||||||
|
if w.totalWeight > 0 {
|
||||||
|
w.lk.Lock()
|
||||||
|
randomWeight := w.randObj.Intn(w.totalWeight)
|
||||||
|
w.lk.Unlock()
|
||||||
|
weightCount := 0
|
||||||
|
for _, object := range w.WeightObject {
|
||||||
|
weightCount += object.Weight
|
||||||
|
if weightCount > randomWeight {
|
||||||
|
return object.Obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var noop T
|
||||||
|
return noop
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogHook struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHook(name string) LogHook {
|
||||||
|
return LogHook{
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LogHook) DialHook(next redis.DialHook) redis.DialHook {
|
||||||
|
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return next(ctx, network, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||||
|
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||||
|
log.Println(cmd.String())
|
||||||
|
return next(ctx, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
|
||||||
|
return func(ctx context.Context, cmds []redis.Cmder) error {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
log.Println(cmd.String())
|
||||||
|
}
|
||||||
|
return next(ctx, cmds)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,467 @@
|
||||||
|
package sliceutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
. "tinyrdm/backend/utils"
|
||||||
|
"tinyrdm/backend/utils/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get 获取指定索引的值, 如果不存在则返回默认值
|
||||||
|
func Get[S ~[]T, T any](arr S, index int, defaultVal T) T {
|
||||||
|
if index < 0 || index >= len(arr) {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return arr[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除指定索引的元素
|
||||||
|
func Remove[S ~[]T, T any](arr S, index int) S {
|
||||||
|
return append(arr[:index], arr[index+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveIf 移除指定条件的元素
|
||||||
|
func RemoveIf[S ~[]T, T any](arr S, cond func(T) bool) S {
|
||||||
|
l := len(arr)
|
||||||
|
if l <= 0 {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
for i := l - 1; i >= 0; i-- {
|
||||||
|
if cond(arr[i]) {
|
||||||
|
arr = append(arr[:i], arr[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRange 删除从[from, to]部分元素
|
||||||
|
func RemoveRange[S ~[]T, T any](arr S, from, to int) S {
|
||||||
|
return append(arr[:from], arr[to:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find 查找指定条件的元素第一个出现位置
|
||||||
|
func Find[S ~[]T, T any](arr S, matchFunc func(int) bool) (int, bool) {
|
||||||
|
total := len(arr)
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if matchFunc(i) {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyMatch 判断是否有任意元素符合条件
|
||||||
|
func AnyMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
|
||||||
|
total := len(arr)
|
||||||
|
if total > 0 {
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if matchFunc(i) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllMatch 判断是否所有元素都符合条件
|
||||||
|
func AllMatch[S ~[]T, T any](arr S, matchFunc func(int) bool) bool {
|
||||||
|
total := len(arr)
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if !matchFunc(i) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals 比较两个切片内容是否完全一致
|
||||||
|
func Equals[S ~[]T, T comparable](arr1, arr2 S) bool {
|
||||||
|
if &arr1 == &arr2 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
len1, len2 := len(arr1), len(arr2)
|
||||||
|
if len1 != len2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len1; i++ {
|
||||||
|
if arr1[i] != arr2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains 判断数组是否包含指定元素
|
||||||
|
func Contains[S ~[]T, T Hashable](arr S, elem T) bool {
|
||||||
|
return AnyMatch(arr, func(idx int) bool {
|
||||||
|
return arr[idx] == elem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsAny 判断数组是否包含任意指定元素
|
||||||
|
func ContainsAny[S ~[]T, T Hashable](arr S, elems ...T) bool {
|
||||||
|
for _, elem := range elems {
|
||||||
|
if Contains(arr, elem) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsAll 判断数组是否包含所有指定元素
|
||||||
|
func ContainsAll[S ~[]T, T Hashable](arr S, elems ...T) bool {
|
||||||
|
for _, elem := range elems {
|
||||||
|
if !Contains(arr, elem) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter 筛选出符合指定条件的所有元素
|
||||||
|
func Filter[S ~[]T, T any](arr S, filterFunc func(int) bool) []T {
|
||||||
|
total := len(arr)
|
||||||
|
var result []T
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if filterFunc(i) {
|
||||||
|
result = append(result, arr[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map 数组映射转换
|
||||||
|
func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
|
||||||
|
total := len(arr)
|
||||||
|
result := make([]R, total)
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
result[i] = mappingFunc(i)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterMap 数组过滤和映射转换
|
||||||
|
func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bool)) []R {
|
||||||
|
total := len(arr)
|
||||||
|
result := make([]R, 0, total)
|
||||||
|
var filter bool
|
||||||
|
var mapItem R
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if mapItem, filter = mappingFunc(i); filter {
|
||||||
|
result = append(result, mapItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMap 数组转键值对
|
||||||
|
func ToMap[S ~[]T, T any, K Hashable, V any](arr S, mappingFunc func(int) (K, V)) map[K]V {
|
||||||
|
total := len(arr)
|
||||||
|
result := map[K]V{}
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
key, val := mappingFunc(i)
|
||||||
|
result[key] = val
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat 二维数组扁平化
|
||||||
|
func Flat[T any](arr [][]T) []T {
|
||||||
|
total := len(arr)
|
||||||
|
var result []T
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
subTotal := len(arr[i])
|
||||||
|
for j := 0; j < subTotal; j++ {
|
||||||
|
result = append(result, arr[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMap 二维数组扁平化映射
|
||||||
|
func FlatMap[T any, R any](arr [][]T, mappingFunc func(int, int) R) []R {
|
||||||
|
total := len(arr)
|
||||||
|
var result []R
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
subTotal := len(arr[i])
|
||||||
|
for j := 0; j < subTotal; j++ {
|
||||||
|
result = append(result, mappingFunc(i, j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlatValueMap[T Hashable](arr [][]T) []T {
|
||||||
|
return FlatMap(arr, func(i, j int) T {
|
||||||
|
return arr[i][j]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce 数组累计
|
||||||
|
func Reduce[S ~[]T, T any, R any](arr S, init R, reduceFunc func(R, T) R) R {
|
||||||
|
result := init
|
||||||
|
for _, item := range arr {
|
||||||
|
result = reduceFunc(result, item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse 反转数组(会修改原数组)
|
||||||
|
func Reverse[S ~[]T, T any](arr S) S {
|
||||||
|
total := len(arr)
|
||||||
|
for i := 0; i < total/2; i++ {
|
||||||
|
arr[i], arr[total-i-1] = arr[total-i-1], arr[i]
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join 数组拼接转字符串
|
||||||
|
func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) string) string {
|
||||||
|
total := len(arr)
|
||||||
|
if total <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if total == 1 {
|
||||||
|
return toStringFunc(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.Builder{}
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
if i != 0 {
|
||||||
|
sb.WriteString(sep)
|
||||||
|
}
|
||||||
|
sb.WriteString(toStringFunc(i))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinString 字符串数组拼接成字符串
|
||||||
|
func JoinString(arr []string, sep string) string {
|
||||||
|
return Join(arr, sep, func(idx int) string {
|
||||||
|
return arr[idx]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinInt 整形数组拼接转字符串
|
||||||
|
func JoinInt(arr []int, sep string) string {
|
||||||
|
return Join(arr, sep, func(idx int) string {
|
||||||
|
return strconv.Itoa(arr[idx])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unique 数组去重
|
||||||
|
func Unique[S ~[]T, T Hashable](arr S) S {
|
||||||
|
result := make(S, 0, len(arr))
|
||||||
|
uniKeys := map[T]struct{}{}
|
||||||
|
var exists bool
|
||||||
|
for _, item := range arr {
|
||||||
|
if _, exists = uniKeys[item]; !exists {
|
||||||
|
uniKeys[item] = struct{}{}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// UniqueEx 数组去重(任意类型)
|
||||||
|
// @param toKeyFunc 数组元素转为唯一标识字符串函数, 如转为哈希值等
|
||||||
|
func UniqueEx[S ~[]T, T any](arr S, toKeyFunc func(i int) string) S {
|
||||||
|
result := make(S, 0, len(arr))
|
||||||
|
keyArr := Map(arr, toKeyFunc)
|
||||||
|
uniKeys := map[string]struct{}{}
|
||||||
|
var exists bool
|
||||||
|
for i, item := range arr {
|
||||||
|
if _, exists = uniKeys[keyArr[i]]; !exists {
|
||||||
|
uniKeys[keyArr[i]] = struct{}{}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort 顺序排序(会修改原数组)
|
||||||
|
func Sort[S ~[]T, T Hashable](arr S) S {
|
||||||
|
sort.Slice(arr, func(i, j int) bool {
|
||||||
|
return arr[i] <= arr[j]
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortDesc 倒序排序(会修改原数组)
|
||||||
|
func SortDesc[S ~[]T, T Hashable](arr S) S {
|
||||||
|
sort.Slice(arr, func(i, j int) bool {
|
||||||
|
return arr[i] > arr[j]
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union 返回两个切片共同拥有的元素
|
||||||
|
func Union[S ~[]T, T Hashable](arr1 S, arr2 S) S {
|
||||||
|
hashArr, compArr := arr1, arr2
|
||||||
|
if len(arr1) < len(arr2) {
|
||||||
|
hashArr, compArr = compArr, hashArr
|
||||||
|
}
|
||||||
|
hash := map[T]struct{}{}
|
||||||
|
for _, item := range hashArr {
|
||||||
|
hash[item] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
uniq := map[T]struct{}{}
|
||||||
|
ret := make(S, 0, len(compArr))
|
||||||
|
exists := false
|
||||||
|
for _, item := range compArr {
|
||||||
|
if _, exists = hash[item]; exists {
|
||||||
|
if _, exists = uniq[item]; !exists {
|
||||||
|
ret = append(ret, item)
|
||||||
|
uniq[item] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude 返回不包含的元素
|
||||||
|
func Exclude[S ~[]T, T Hashable](arr1 S, arr2 S) S {
|
||||||
|
diff := make([]T, 0, len(arr1))
|
||||||
|
hash := map[T]struct{}{}
|
||||||
|
for _, item := range arr2 {
|
||||||
|
hash[item] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range arr1 {
|
||||||
|
if _, exists := hash[item]; !exists {
|
||||||
|
diff = append(diff, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadLeft 左边填充指定数量
|
||||||
|
func PadLeft[S ~[]T, T any](arr S, val T, count int) S {
|
||||||
|
prefix := make(S, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
prefix[i] = val
|
||||||
|
}
|
||||||
|
arr = append(prefix, arr...)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadRight 右边填充指定数量
|
||||||
|
func PadRight[S ~[]T, T any](arr S, val T, count int) S {
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
arr = append(arr, val)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveLeft 移除左侧相同元素
|
||||||
|
func RemoveLeft[S ~[]T, T comparable](arr S, val T) S {
|
||||||
|
for len(arr) > 0 && arr[0] == val {
|
||||||
|
arr = arr[1:]
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRight 移除右侧相同元素
|
||||||
|
func RemoveRight[S ~[]T, T comparable](arr S, val T) S {
|
||||||
|
for {
|
||||||
|
length := len(arr)
|
||||||
|
if length > 0 && arr[length-1] == val {
|
||||||
|
arr = arr[:length]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomElem 从切片中随机抽一个
|
||||||
|
func RandomElem[S ~[]T, T any](arr S) T {
|
||||||
|
l := len(arr)
|
||||||
|
if l <= 0 {
|
||||||
|
var r T
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return arr[rand.Intn(l)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomElems 从切片中随机抽多个
|
||||||
|
// 如果切片长度为空, 则返回空切片
|
||||||
|
func RandomElems[S ~[]T, T any](arr S, count int) []T {
|
||||||
|
l := len(arr)
|
||||||
|
ret := make([]T, 0, l)
|
||||||
|
if l <= 0 {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
idxList := rand.IntnCount(l, count)
|
||||||
|
for _, idx := range idxList {
|
||||||
|
ret = append(ret, arr[idx])
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomUniqElems 从切片中随机抽多个不同的元素
|
||||||
|
// 如果切片长度为空, 则返回空切片
|
||||||
|
// 如果所需数量大于切片唯一元素数量, 则返回整个切片
|
||||||
|
func RandomUniqElems[S ~[]T, T Hashable](arr S, count int) []T {
|
||||||
|
if len(arr) <= 0 {
|
||||||
|
// 可选列表为空, 返回空切片
|
||||||
|
return []T{}
|
||||||
|
}
|
||||||
|
// 转换为集合
|
||||||
|
uniqList := Unique(arr)
|
||||||
|
uniqLen := len(uniqList)
|
||||||
|
if uniqLen <= count {
|
||||||
|
// 可选集合总数<=所需元素数量, 直接返回整个可选集合
|
||||||
|
return uniqList
|
||||||
|
}
|
||||||
|
|
||||||
|
if count >= uniqLen/2 {
|
||||||
|
// 所需唯一元素大于可选集合一半, 随机筛掉(uniqLen-count)个元素
|
||||||
|
for i := 0; i < uniqLen-count; i++ {
|
||||||
|
uniqList = Remove(uniqList, rand.Intn(uniqLen-i))
|
||||||
|
}
|
||||||
|
return uniqList
|
||||||
|
} else {
|
||||||
|
// 所需唯一元素小于可选集合一半, 随机抽取count个元素
|
||||||
|
res := make([]T, count)
|
||||||
|
var idx int
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
idx = rand.Intn(uniqLen - i)
|
||||||
|
res[i] = uniqList[idx]
|
||||||
|
uniqList = Remove(uniqList, idx)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone 复制切片
|
||||||
|
func Clone[S ~[]T, T any](src S) S {
|
||||||
|
dest := make(S, len(src))
|
||||||
|
copy(dest, src)
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count 统计制定条件元素数量
|
||||||
|
func Count[S ~[]T, T any](arr S, filter func(int) bool) int {
|
||||||
|
count := 0
|
||||||
|
for i := range arr {
|
||||||
|
if filter(i) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group 根据分组函数对数组进行分组汇总
|
||||||
|
func Group[S ~[]T, T any, K Hashable, R any](arr S, groupFunc func(int) (K, R)) map[K][]R {
|
||||||
|
ret := map[K][]R{}
|
||||||
|
for i := range arr {
|
||||||
|
key, val := groupFunc(i)
|
||||||
|
ret[key] = append(ret[key], val)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Build Directory
|
||||||
|
|
||||||
|
The build directory is used to house all the build files and assets for your application.
|
||||||
|
|
||||||
|
The structure is:
|
||||||
|
|
||||||
|
* bin - Output directory
|
||||||
|
* darwin - macOS specific files
|
||||||
|
* windows - Windows specific files
|
||||||
|
|
||||||
|
## Mac
|
||||||
|
|
||||||
|
The `darwin` directory holds files specific to Mac builds.
|
||||||
|
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||||
|
and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
The directory contains the following files:
|
||||||
|
|
||||||
|
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||||
|
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||||
|
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||||
|
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||||
|
will be created using the `appicon.png` file in the build directory.
|
||||||
|
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||||
|
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||||
|
as well as the application itself (right click the exe -> properties -> details)
|
||||||
|
- `wails.exe.manifest` - The main application manifest file.
|
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "{{.Info.ProductVersion}}"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||||
|
"CompanyName": "{{.Info.CompanyName}}",
|
||||||
|
"FileDescription": "{{.Info.ProductName}}",
|
||||||
|
"LegalCopyright": "{{.Info.Copyright}}",
|
||||||
|
"ProductName": "{{.Info.ProductName}}",
|
||||||
|
"Comments": "{{.Info.Comments}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||||
|
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||||
|
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||||
|
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
|
@ -0,0 +1,179 @@
|
||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "{{.Name}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||||
|
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs,
|
||||||
|
check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta charset='UTF-8' />
|
||||||
|
<meta content='width=device-width, initial-scale=1.0' name='viewport' />
|
||||||
|
<title>Tiny RDM</title>
|
||||||
|
<!-- <link href="./src/style.scss" rel="stylesheet">-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='app'></div>
|
||||||
|
<script src='./src/main.js' type='module'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"highlight.js": "^11.8.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"pinia": "^2.1.3",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"vue": "^3.2.37",
|
||||||
|
"vue-i18n": "^9.2.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"naive-ui": "^2.34.4",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"unplugin-auto-import": "^0.16.4",
|
||||||
|
"unplugin-icons": "^0.16.1",
|
||||||
|
"unplugin-vue-components": "^0.25.0",
|
||||||
|
"vite": "^4.3.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
da66eb9d13a7ace25f7f75d36c2510f9
|
|
@ -0,0 +1,160 @@
|
||||||
|
<script setup>
|
||||||
|
import { get } from 'lodash'
|
||||||
|
import { computed, nextTick, onMounted, provide, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { GetPreferences } from '../wailsjs/go/storage/PreferencesStorage.js'
|
||||||
|
import ContentPane from './components/ContentPane.vue'
|
||||||
|
import NewConnDialog from './components/dialogs/NewConnDialog.vue'
|
||||||
|
import NewKeyDialog from './components/dialogs/NewKeyDialog.vue'
|
||||||
|
import PreferencesDialog from './components/dialogs/PreferencesDialog.vue'
|
||||||
|
import RenameKeyDialog from './components/dialogs/RenameKeyDialog.vue'
|
||||||
|
import SetTtlDialog from './components/dialogs/SetTtlDialog.vue'
|
||||||
|
import NavigationPane from './components/NavigationPane.vue'
|
||||||
|
import hljs from 'highlight.js/lib/core'
|
||||||
|
import json from 'highlight.js/lib/languages/json'
|
||||||
|
import plaintext from 'highlight.js/lib/languages/plaintext'
|
||||||
|
import { useThemeVars } from 'naive-ui'
|
||||||
|
import AddFieldsDialog from './components/dialogs/AddFieldsDialog.vue'
|
||||||
|
|
||||||
|
const themeVars = useThemeVars()
|
||||||
|
|
||||||
|
hljs.registerLanguage('json', json)
|
||||||
|
hljs.registerLanguage('plaintext', plaintext)
|
||||||
|
|
||||||
|
const data = reactive({
|
||||||
|
asideWith: 300,
|
||||||
|
hoverResize: false,
|
||||||
|
resizing: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const preferences = ref({})
|
||||||
|
provide('preferences', preferences)
|
||||||
|
const i18n = useI18n()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
preferences.value = await GetPreferences()
|
||||||
|
await nextTick(() => {
|
||||||
|
i18n.locale.value = get(preferences.value, 'general.language', 'en')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: apply font size to all elements
|
||||||
|
const getFontSize = computed(() => {
|
||||||
|
return get(preferences.value, 'general.font_size', 'en')
|
||||||
|
})
|
||||||
|
|
||||||
|
const themeOverrides = {
|
||||||
|
common: {
|
||||||
|
// primaryColor: '#409EFF',
|
||||||
|
borderRadius: '4px',
|
||||||
|
borderRadiusSmall: '3px',
|
||||||
|
fontFamily: `"Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue"`,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
Tag: {
|
||||||
|
// borderRadius: '3px'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = (evt) => {
|
||||||
|
if (data.resizing) {
|
||||||
|
data.asideWith = Math.max(evt.clientX, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
data.resizing = false
|
||||||
|
document.removeEventListener('mousemove', handleResize)
|
||||||
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
// TODO: Save sidebar x-position
|
||||||
|
}
|
||||||
|
|
||||||
|
const startResize = () => {
|
||||||
|
data.resizing = true
|
||||||
|
document.addEventListener('mousemove', handleResize)
|
||||||
|
document.addEventListener('mouseup', stopResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const asideWidthVal = computed(() => {
|
||||||
|
return data.asideWith + 'px'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dragging = computed(() => {
|
||||||
|
return data.hoverResize || data.resizing
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-config-provider :hljs="hljs" :inline-theme-disabled="true" :theme-overrides="themeOverrides" class="fill-height">
|
||||||
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<div id="app-container" :class="{ dragging: dragging }" class="flex-box-h">
|
||||||
|
<div id="app-side" :style="{ width: asideWidthVal }" class="flex-box-h flex-item">
|
||||||
|
<navigation-pane class="flex-item-expand"></navigation-pane>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'resize-divider-hover': data.hoverResize,
|
||||||
|
'resize-divider-drag': data.resizing,
|
||||||
|
}"
|
||||||
|
class="resize-divider"
|
||||||
|
@mousedown="startResize"
|
||||||
|
@mouseout="data.hoverResize = false"
|
||||||
|
@mouseover="data.hoverResize = true"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<content-pane class="flex-item-expand" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- top modal dialogs -->
|
||||||
|
<new-conn-dialog />
|
||||||
|
<new-key-dialog />
|
||||||
|
<add-fields-dialog />
|
||||||
|
<rename-key-dialog />
|
||||||
|
<set-ttl-dialog />
|
||||||
|
<preferences-dialog />
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#app-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: var(--border-color) 1px solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
#app-toolbar {
|
||||||
|
height: 40px;
|
||||||
|
border-bottom: var(--border-color) 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-side {
|
||||||
|
//overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.resize-divider {
|
||||||
|
//height: 100%;
|
||||||
|
width: 2px;
|
||||||
|
border-left-width: 5px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider-hover {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-divider-drag {
|
||||||
|
//background-color: rgb(0, 105, 218);
|
||||||
|
width: 5px;
|
||||||
|
//background-color: var(--el-color-primary);
|
||||||
|
background-color: v-bind('themeVars.primaryColor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging {
|
||||||
|
cursor: col-resize !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
|
@ -0,0 +1,450 @@
|
||||||
|
<script setup>
|
||||||
|
import { h, nextTick, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ConnectionType } from '../consts/connection_type.js'
|
||||||
|
import useConnection from '../stores/connection.js'
|
||||||
|
import { NIcon, useDialog, useMessage } from 'naive-ui'
|
||||||
|
import ToggleFolder from './icons/ToggleFolder.vue'
|
||||||
|
import Key from './icons/Key.vue'
|
||||||
|
import ToggleDb from './icons/ToggleDb.vue'
|
||||||
|
import ToggleServer from './icons/ToggleServer.vue'
|
||||||
|
import { indexOf, remove, startsWith } from 'lodash'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Refresh from './icons/Refresh.vue'
|
||||||
|
import Config from './icons/Config.vue'
|
||||||
|
import CopyLink from './icons/CopyLink.vue'
|
||||||
|
import Unlink from './icons/Unlink.vue'
|
||||||
|
import Add from './icons/Add.vue'
|
||||||
|
import Layer from './icons/Layer.vue'
|
||||||
|
import Delete from './icons/Delete.vue'
|
||||||
|
import Connect from './icons/Connect.vue'
|
||||||
|
import useDialogStore from '../stores/dialog.js'
|
||||||
|
import { ClipboardSetText } from '../../wailsjs/runtime/runtime.js'
|
||||||
|
import useTabStore from '../stores/tab.js'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingConnections = ref(false)
|
||||||
|
const expandedKeys = ref([])
|
||||||
|
const connectionStore = useConnection()
|
||||||
|
const tabStore = useTabStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
const showContextMenu = ref(false)
|
||||||
|
const contextPos = reactive({ x: 0, y: 0 })
|
||||||
|
const contextMenuOptions = ref(null)
|
||||||
|
const currentContextNode = ref(null)
|
||||||
|
const renderIcon = (icon) => {
|
||||||
|
return () => {
|
||||||
|
return h(NIcon, null, {
|
||||||
|
default: () => h(icon),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const menuOptions = {
|
||||||
|
[ConnectionType.Group]: ({ opened }) => [
|
||||||
|
{
|
||||||
|
key: 'group_reload',
|
||||||
|
label: i18n.t('config_conn_group'),
|
||||||
|
icon: renderIcon(Config),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'group_delete',
|
||||||
|
label: i18n.t('remove_conn_group'),
|
||||||
|
icon: renderIcon(Delete),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[ConnectionType.Server]: ({ connected }) => {
|
||||||
|
if (connected) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'server_reload',
|
||||||
|
label: i18n.t('reload'),
|
||||||
|
icon: renderIcon(Refresh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'server_disconnect',
|
||||||
|
label: i18n.t('disconnect'),
|
||||||
|
icon: renderIcon(Unlink),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'server_dup',
|
||||||
|
label: i18n.t('dup_conn'),
|
||||||
|
icon: renderIcon(CopyLink),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'server_config',
|
||||||
|
label: i18n.t('config_conn'),
|
||||||
|
icon: renderIcon(Config),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'server_remove',
|
||||||
|
label: i18n.t('remove_conn'),
|
||||||
|
icon: renderIcon(Delete),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'server_open',
|
||||||
|
label: i18n.t('open_connection'),
|
||||||
|
icon: renderIcon(Connect),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ConnectionType.RedisDB]: ({ opened }) => {
|
||||||
|
if (opened) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'db_reload',
|
||||||
|
label: i18n.t('reload'),
|
||||||
|
icon: renderIcon(Refresh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'db_newkey',
|
||||||
|
label: i18n.t('new_key'),
|
||||||
|
icon: renderIcon(Add),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'db_open',
|
||||||
|
label: i18n.t('open_db'),
|
||||||
|
icon: renderIcon(Connect),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ConnectionType.RedisKey]: () => [
|
||||||
|
{
|
||||||
|
key: 'key_reload',
|
||||||
|
label: i18n.t('reload'),
|
||||||
|
icon: renderIcon(Refresh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key_newkey',
|
||||||
|
label: i18n.t('new_key'),
|
||||||
|
icon: renderIcon(Add),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key_copy',
|
||||||
|
label: i18n.t('copy_path'),
|
||||||
|
icon: renderIcon(CopyLink),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'key_remove',
|
||||||
|
label: i18n.t('remove_path'),
|
||||||
|
icon: renderIcon(Delete),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[ConnectionType.RedisValue]: () => [
|
||||||
|
{
|
||||||
|
key: 'value_reload',
|
||||||
|
label: i18n.t('reload'),
|
||||||
|
icon: renderIcon(Refresh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'value_copy',
|
||||||
|
label: i18n.t('copy_key'),
|
||||||
|
icon: renderIcon(CopyLink),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'value_remove',
|
||||||
|
label: i18n.t('remove_key'),
|
||||||
|
icon: renderIcon(Delete),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContextLabel = (option) => {
|
||||||
|
return h('div', { class: 'context-menu-item' }, option.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// TODO: Show loading list status
|
||||||
|
loadingConnections.value = true
|
||||||
|
nextTick(connectionStore.initConnection)
|
||||||
|
} finally {
|
||||||
|
loadingConnections.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expandKey = (key) => {
|
||||||
|
const idx = indexOf(expandedKeys.value, key)
|
||||||
|
if (idx === -1) {
|
||||||
|
expandedKeys.value.push(key)
|
||||||
|
} else {
|
||||||
|
expandedKeys.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapseKeyAndChildren = (key) => {
|
||||||
|
remove(expandedKeys.value, (k) => startsWith(k, key))
|
||||||
|
// console.log(key)
|
||||||
|
// const idx = indexOf(expandedKeys.value, key)
|
||||||
|
// console.log(JSON.stringify(expandedKeys.value))
|
||||||
|
// if (idx !== -1) {
|
||||||
|
// expandedKeys.value.splice(idx, 1)
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const onUpdateExpanded = (value, option, meta) => {
|
||||||
|
expandedKeys.value = value
|
||||||
|
if (!meta.node) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// console.log(JSON.stringify(meta))
|
||||||
|
switch (meta.action) {
|
||||||
|
case 'expand':
|
||||||
|
meta.node.expanded = true
|
||||||
|
break
|
||||||
|
case 'collapse':
|
||||||
|
meta.node.expanded = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPrefix = ({ option }) => {
|
||||||
|
switch (option.type) {
|
||||||
|
case ConnectionType.Group:
|
||||||
|
return h(
|
||||||
|
NIcon,
|
||||||
|
{ size: 20 },
|
||||||
|
{
|
||||||
|
default: () => h(ToggleFolder, { modelValue: option.expanded === true }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case ConnectionType.Server:
|
||||||
|
return h(
|
||||||
|
NIcon,
|
||||||
|
{ size: 20 },
|
||||||
|
{
|
||||||
|
default: () => h(ToggleServer, { modelValue: option.connected === true }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case ConnectionType.RedisDB:
|
||||||
|
return h(
|
||||||
|
NIcon,
|
||||||
|
{ size: 20 },
|
||||||
|
{
|
||||||
|
default: () => h(ToggleDb, { modelValue: option.opened === true }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case ConnectionType.RedisKey:
|
||||||
|
return h(
|
||||||
|
NIcon,
|
||||||
|
{ size: 20 },
|
||||||
|
{
|
||||||
|
default: () => h(Layer),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case ConnectionType.RedisValue:
|
||||||
|
return h(
|
||||||
|
NIcon,
|
||||||
|
{ size: 20 },
|
||||||
|
{
|
||||||
|
default: () => h(Key),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderLabel = ({ option }) => {
|
||||||
|
switch (option.type) {
|
||||||
|
case ConnectionType.RedisDB:
|
||||||
|
case ConnectionType.RedisKey:
|
||||||
|
return `${option.label} (${option.keys || 0})`
|
||||||
|
// case ConnectionType.RedisValue:
|
||||||
|
// return `[${option.keyType}]${option.label}`
|
||||||
|
}
|
||||||
|
return option.label
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSuffix = ({ option }) => {
|
||||||
|
// return h(NButton,
|
||||||
|
// { text: true, type: 'primary' },
|
||||||
|
// { default: () => h(Key) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeProps = ({ option }) => {
|
||||||
|
return {
|
||||||
|
onClick() {
|
||||||
|
connectionStore.select(option)
|
||||||
|
// console.log('[click]:' + JSON.stringify(option))
|
||||||
|
},
|
||||||
|
onDblclick: async () => {
|
||||||
|
if (loading.value) {
|
||||||
|
console.warn('TODO: alert to ignore double click when loading')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (option.type) {
|
||||||
|
case ConnectionType.Server:
|
||||||
|
option.isLeaf = false
|
||||||
|
break
|
||||||
|
|
||||||
|
case ConnectionType.RedisDB:
|
||||||
|
option.isLeaf = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// default handle is expand current node
|
||||||
|
nextTick().then(() => expandKey(option.key))
|
||||||
|
},
|
||||||
|
onContextmenu(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const mop = menuOptions[option.type]
|
||||||
|
if (mop == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showContextMenu.value = false
|
||||||
|
nextTick().then(() => {
|
||||||
|
contextMenuOptions.value = mop(option)
|
||||||
|
currentContextNode.value = option
|
||||||
|
contextPos.x = e.clientX
|
||||||
|
contextPos.y = e.clientY
|
||||||
|
showContextMenu.value = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// onMouseover() {
|
||||||
|
// console.log('mouse over')
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoadTree = async (node) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case ConnectionType.Server:
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await connectionStore.openConnection(node.name)
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
node.isLeaf = undefined
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ConnectionType.RedisDB:
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await connectionStore.openDatabase(node.name, node.db)
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
node.isLeaf = undefined
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectContextMenu = (key) => {
|
||||||
|
showContextMenu.value = false
|
||||||
|
const { name, db, key: nodeKey, redisKey } = currentContextNode.value
|
||||||
|
switch (key) {
|
||||||
|
case 'server_disconnect':
|
||||||
|
connectionStore.closeConnection(nodeKey).then((success) => {
|
||||||
|
if (success) {
|
||||||
|
collapseKeyAndChildren(nodeKey)
|
||||||
|
tabStore.removeTabByName(name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
// case 'server_reload':
|
||||||
|
// case 'db_reload':
|
||||||
|
// connectionStore.loadKeyValue()
|
||||||
|
// break
|
||||||
|
case 'db_newkey':
|
||||||
|
case 'key_newkey':
|
||||||
|
dialogStore.openNewKeyDialog(redisKey, name, db)
|
||||||
|
break
|
||||||
|
case 'key_remove':
|
||||||
|
case 'value_remove':
|
||||||
|
dialog.warning({
|
||||||
|
title: i18n.t('warning'),
|
||||||
|
content: i18n.t('delete_key_tip', { key: redisKey }),
|
||||||
|
closable: false,
|
||||||
|
autoFocus: false,
|
||||||
|
transformOrigin: 'center',
|
||||||
|
positiveText: i18n.t('confirm'),
|
||||||
|
negativeText: i18n.t('cancel'),
|
||||||
|
onPositiveClick: () => {
|
||||||
|
connectionStore.removeKey(name, db, redisKey).then((success) => {
|
||||||
|
if (success) {
|
||||||
|
message.success(i18n.t('delete_key_succ', { key: redisKey }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'key_copy':
|
||||||
|
case 'value_copy':
|
||||||
|
ClipboardSetText(redisKey)
|
||||||
|
.then((succ) => {
|
||||||
|
if (succ) {
|
||||||
|
message.success(i18n.t('copy_succ'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
message.error(e.message)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.warn('TODO: handle context menu:' + key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOutsideContextMenu = () => {
|
||||||
|
showContextMenu.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-tree
|
||||||
|
:block-line="true"
|
||||||
|
:block-node="true"
|
||||||
|
:data="connectionStore.connections"
|
||||||
|
:expand-on-click="false"
|
||||||
|
:expanded-keys="expandedKeys"
|
||||||
|
:node-props="nodeProps"
|
||||||
|
:on-load="onLoadTree"
|
||||||
|
:on-update:expanded-keys="onUpdateExpanded"
|
||||||
|
:render-label="renderLabel"
|
||||||
|
:render-prefix="renderPrefix"
|
||||||
|
:render-suffix="renderSuffix"
|
||||||
|
block-line
|
||||||
|
class="fill-height"
|
||||||
|
virtual-scroll
|
||||||
|
/>
|
||||||
|
<n-dropdown
|
||||||
|
:animated="false"
|
||||||
|
:options="contextMenuOptions"
|
||||||
|
:render-label="renderContextLabel"
|
||||||
|
:show="showContextMenu"
|
||||||
|
:x="contextPos.x"
|
||||||
|
:y="contextPos.y"
|
||||||
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
@clickoutside="handleOutsideContextMenu"
|
||||||
|
@select="handleSelectContextMenu"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,111 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { types } from '../consts/support_redis_type.js'
|
||||||
|
import ContentValueHash from './content_value/ContentValueHash.vue'
|
||||||
|
import ContentValueList from './content_value/ContentValueList.vue'
|
||||||
|
import ContentValueString from './content_value/ContentValueString.vue'
|
||||||
|
import ContentValueSet from './content_value/ContentValueSet.vue'
|
||||||
|
import ContentValueZset from './content_value/ContentValueZset.vue'
|
||||||
|
import { isEmpty, map, toUpper } from 'lodash'
|
||||||
|
import useTabStore from '../stores/tab.js'
|
||||||
|
|
||||||
|
const valueComponents = {
|
||||||
|
[types.STRING]: ContentValueString,
|
||||||
|
[types.HASH]: ContentValueHash,
|
||||||
|
[types.LIST]: ContentValueList,
|
||||||
|
[types.SET]: ContentValueSet,
|
||||||
|
[types.ZSET]: ContentValueZset,
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabStore = useTabStore()
|
||||||
|
const tab = computed(() =>
|
||||||
|
map(tabStore.tabs, (item) => ({
|
||||||
|
key: item.name,
|
||||||
|
label: item.title,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {ComputedRef<TabItem>}
|
||||||
|
*/
|
||||||
|
const tabContent = computed(() => {
|
||||||
|
const tab = tabStore.currentTab
|
||||||
|
if (tab == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: tab.name,
|
||||||
|
type: toUpper(tab.type),
|
||||||
|
db: tab.db,
|
||||||
|
keyPath: tab.key,
|
||||||
|
ttl: tab.ttl,
|
||||||
|
value: tab.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onUpdateValue = (tabIndex) => {
|
||||||
|
tabStore.switchTab(tabIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAddTab = () => {
|
||||||
|
tabStore.newBlankTab()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCloseTab = (tabIndex) => {
|
||||||
|
tabStore.removeTab(tabIndex)
|
||||||
|
console.log('TODO: close connection also')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-container flex-box-v">
|
||||||
|
<!-- <content-tab :model-value="tab"></content-tab>-->
|
||||||
|
<n-tabs
|
||||||
|
v-model:value="tabStore.activatedIndex"
|
||||||
|
:closable="tab.length > 1"
|
||||||
|
addable
|
||||||
|
size="small"
|
||||||
|
type="card"
|
||||||
|
@add="onAddTab"
|
||||||
|
@close="onCloseTab"
|
||||||
|
@update:value="onUpdateValue"
|
||||||
|
>
|
||||||
|
<n-tab v-for="(t, i) in tab" :key="i" :name="i">
|
||||||
|
<n-ellipsis style="max-width: 150px">{{ t.label }}</n-ellipsis>
|
||||||
|
</n-tab>
|
||||||
|
</n-tabs>
|
||||||
|
<!-- add loading status -->
|
||||||
|
<component
|
||||||
|
:is="valueComponents[tabContent.type]"
|
||||||
|
v-if="tabContent != null && !isEmpty(tabContent.keyPath)"
|
||||||
|
:db="tabContent.db"
|
||||||
|
:key-path="tabContent.keyPath"
|
||||||
|
:name="tabContent.name"
|
||||||
|
:ttl="tabContent.ttl"
|
||||||
|
:value="tabContent.value"
|
||||||
|
/>
|
||||||
|
<div v-else class="flex-item-expand flex-box-v">
|
||||||
|
<n-empty :description="$t('empty_tab_content')" class="empty-content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.content-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import useConnectionStore from '../stores/connection.js'
|
||||||
|
import { throttle } from 'lodash'
|
||||||
|
import { ConnectionType } from '../consts/connection_type.js'
|
||||||
|
import Close from './icons/Close.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['switchTab', 'closeTab', 'update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectedIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: [
|
||||||
|
{
|
||||||
|
// label: 'tab1',
|
||||||
|
// key: 'key',
|
||||||
|
// bgColor: 'white',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const onCurrentSelectChange = ({ type, group = '', server = '', db = 0, key = '' }) => {
|
||||||
|
console.log(`group: ${group}\n server: ${server}\n db: ${db}\n key: ${key}`)
|
||||||
|
if (type === ConnectionType.RedisValue) {
|
||||||
|
// load and update content value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(() => connectionStore.currentSelect, throttle(onCurrentSelectChange, 1000))
|
||||||
|
|
||||||
|
const items = ref(props.modelValue)
|
||||||
|
const selIndex = ref(props.selectedIndex)
|
||||||
|
|
||||||
|
const onClickTab = (idx, key) => {
|
||||||
|
if (idx !== selIndex.value) {
|
||||||
|
selIndex.value = idx
|
||||||
|
emit('update:modelValue', idx, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCloseTab = (idx, key) => {
|
||||||
|
const removed = items.value.splice(idx, 1)
|
||||||
|
if (removed.length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update select index if removed index equal current selected
|
||||||
|
if (selIndex.value === idx) {
|
||||||
|
selIndex.value -= 1
|
||||||
|
if (selIndex.value < 0 && items.value.length > 0) {
|
||||||
|
selIndex.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit('update:modelValue', items)
|
||||||
|
emit('closeTab', idx, key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- TODO: 检查标签是否太多, 左右两边显示左右切换翻页按钮 -->
|
||||||
|
<div class="content-tab flex-box-h">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in props.modelValue"
|
||||||
|
:key="item.key"
|
||||||
|
:class="{ 'content-tab_selected': selIndex === i }"
|
||||||
|
:style="{ backgroundColor: item.bgColor || '' }"
|
||||||
|
:title="item.label"
|
||||||
|
class="content-tab_item flex-item-expand icon-btn flex-box-h"
|
||||||
|
@click="onClickTab(i, item.key)"
|
||||||
|
>
|
||||||
|
<n-icon :component="Close" class="content-tab_item-close" size="20" @click.stop="onCloseTab(i, item.key)" />
|
||||||
|
<div class="content-tab_item-label ellipsis flex-item-expand">
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.content-tab {
|
||||||
|
align-items: center;
|
||||||
|
//justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&_item {
|
||||||
|
flex: 1 0;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3px;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
padding: 0 5px;
|
||||||
|
|
||||||
|
//border-top: var(--el-border-color) 1px solid;
|
||||||
|
border-right: var(--border-color) 1px solid;
|
||||||
|
transition: all var(--transition-duration-fast) var(--transition-function-ease-in-out-bezier);
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-close {
|
||||||
|
//display: none;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 0;
|
||||||
|
transition: width 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(176, 177, 182, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.content-tab_item-close {
|
||||||
|
//display: block;
|
||||||
|
width: 20px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_selected {
|
||||||
|
border-top: #409eff 4px solid !important;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,107 @@
|
||||||
|
<script setup>
|
||||||
|
import { validType } from '../consts/support_redis_type'
|
||||||
|
import useDialog from '../stores/dialog'
|
||||||
|
import Delete from './icons/Delete.vue'
|
||||||
|
import Edit from './icons/Edit.vue'
|
||||||
|
import Refresh from './icons/Refresh.vue'
|
||||||
|
import Timer from './icons/Timer.vue'
|
||||||
|
import RedisTypeTag from './RedisTypeTag.vue'
|
||||||
|
import useConnectionStore from '../stores/connection.js'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import IconButton from './IconButton.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
server: String,
|
||||||
|
db: Number,
|
||||||
|
keyType: {
|
||||||
|
type: String,
|
||||||
|
validator(value) {
|
||||||
|
return validType(value)
|
||||||
|
},
|
||||||
|
default: 'STRING',
|
||||||
|
},
|
||||||
|
keyPath: String,
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const message = useMessage()
|
||||||
|
const i18n = useI18n()
|
||||||
|
|
||||||
|
const onReloadKey = () => {
|
||||||
|
connectionStore.loadKeyValue(props.server, props.db, props.keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirmDelete = async () => {
|
||||||
|
const success = await connectionStore.removeKey(props.server, props.db, props.keyPath)
|
||||||
|
if (success) {
|
||||||
|
message.success(i18n.t('delete_key_succ', { key: props.keyPath }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-toolbar flex-box-h">
|
||||||
|
<n-input-group>
|
||||||
|
<redis-type-tag :type="props.keyType" size="large"></redis-type-tag>
|
||||||
|
<n-input v-model:value="props.keyPath">
|
||||||
|
<template #suffix>
|
||||||
|
<icon-button :icon="Refresh" :tooltip="$t('reload_key')" size="18" @click="onReloadKey" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
</n-input-group>
|
||||||
|
<n-button-group>
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button @click="dialogStore.openTTLDialog(props.ttl)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Timer" size="18" />
|
||||||
|
</template>
|
||||||
|
<template v-if="ttl < 0">
|
||||||
|
{{ $t('forever') }}
|
||||||
|
</template>
|
||||||
|
<template v-else> {{ ttl }} {{ $t('second') }}</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
TTL
|
||||||
|
</n-tooltip>
|
||||||
|
<n-button @click="dialogStore.openRenameKeyDialog(props.server, props.db, props.keyPath)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Edit" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('rename_key') }}
|
||||||
|
</n-button>
|
||||||
|
</n-button-group>
|
||||||
|
<n-tooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<n-popconfirm
|
||||||
|
:negative-text="$t('cancel')"
|
||||||
|
:positive-text="$t('confirm')"
|
||||||
|
@positive-click="onConfirmDelete"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Delete" size="18" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ $t('delete_key_tip', { key: props.keyPath }) }}
|
||||||
|
</n-popconfirm>
|
||||||
|
</template>
|
||||||
|
{{ $t('delete_key') }}
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.content-toolbar {
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script setup>
|
||||||
|
import IconButton from './IconButton.vue'
|
||||||
|
import Delete from './icons/Delete.vue'
|
||||||
|
import Edit from './icons/Edit.vue'
|
||||||
|
import Close from './icons/Close.vue'
|
||||||
|
import Save from './icons/Save.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
bindKey: String,
|
||||||
|
editing: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['edit', 'delete', 'save', 'cancel'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- TODO: support multiple save -->
|
||||||
|
<div v-if="props.editing" class="flex-box-h edit-column-func">
|
||||||
|
<icon-button :icon="Save" @click="emit('save')" />
|
||||||
|
<icon-button :icon="Close" @click="emit('cancel')" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex-box-h edit-column-func">
|
||||||
|
<icon-button :icon="Edit" @click="emit('edit')" />
|
||||||
|
<n-popconfirm :negative-text="$t('cancel')" :positive-text="$t('confirm')" @positive-click="emit('delete')">
|
||||||
|
<template #trigger>
|
||||||
|
<icon-button :icon="Delete" />
|
||||||
|
</template>
|
||||||
|
{{ $t('delete_key_tip', { key: props.bindKey }) }}
|
||||||
|
</n-popconfirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.edit-column-func {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup>
|
||||||
|
import { NInput } from 'naive-ui'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isEdit: Boolean,
|
||||||
|
value: [String, Number],
|
||||||
|
onUpdateValue: [Function, Array],
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
const handleUpdateValue = (val) => {
|
||||||
|
emit('update:value', val)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="min-height: 22px">
|
||||||
|
<template v-if="props.isEdit">
|
||||||
|
<!-- TODO: ADD FULL SCREEN EDIT SUPPORT -->
|
||||||
|
<n-input :value="props.value" @update:value="handleUpdateValue" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ props.value }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { NIcon } from 'naive-ui'
|
||||||
|
|
||||||
|
const emit = defineEmits(['click'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tooltip: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
tTooltip: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: [String, Object],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'currentColor',
|
||||||
|
},
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasTooltip = computed(() => {
|
||||||
|
return props.tooltip || props.tTooltip
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-tooltip v-if="hasTooltip">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :color="props.color" :size="props.size" class="icon-btn" @click="emit('click')">
|
||||||
|
<component :is="props.icon" :stroke-width="props.strokeWidth"></component>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
|
||||||
|
</n-tooltip>
|
||||||
|
<n-icon v-else :color="props.color" :size="props.size" class="icon-btn" @click="emit('click')">
|
||||||
|
<component :is="props.icon" :stroke-width="props.strokeWidth"></component>
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script setup>
|
||||||
|
import useDialogStore from '../stores/dialog.js'
|
||||||
|
import { NIcon } from 'naive-ui'
|
||||||
|
import AddGroup from './icons/AddGroup.vue'
|
||||||
|
import AddLink from './icons/AddLink.vue'
|
||||||
|
import Sort from './icons/Sort.vue'
|
||||||
|
import ConnectionsTree from './ConnectionsTree.vue'
|
||||||
|
import IconButton from './IconButton.vue'
|
||||||
|
import Filter from './icons/Filter.vue'
|
||||||
|
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
const onSort = () => {
|
||||||
|
dialogStore.openPreferencesDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="nav-pane-container flex-box-v">
|
||||||
|
<ConnectionsTree />
|
||||||
|
|
||||||
|
<!-- bottom function bar -->
|
||||||
|
<div class="nav-pane-bottom flex-box-h">
|
||||||
|
<icon-button
|
||||||
|
:icon="AddLink"
|
||||||
|
color="#555"
|
||||||
|
size="20"
|
||||||
|
stroke-width="4"
|
||||||
|
t-tooltip="new_conn"
|
||||||
|
@click="dialogStore.openNewDialog()"
|
||||||
|
/>
|
||||||
|
<icon-button
|
||||||
|
:icon="AddGroup"
|
||||||
|
color="#555"
|
||||||
|
size="20"
|
||||||
|
stroke-width="4"
|
||||||
|
t-tooltip="new_group"
|
||||||
|
@click="dialogStore.openNewKeyDialog('aa:bb')"
|
||||||
|
/>
|
||||||
|
<n-divider style="margin: 0 4px; --n-color: #aaa; width: 2px" vertical />
|
||||||
|
<icon-button :icon="Sort" color="#555" size="20" stroke-width="4" t-tooltip="sort_conn" @click="onSort" />
|
||||||
|
<n-input placeholder="">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="Filter" color="#aaa" size="20" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nav-pane-container {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
|
||||||
|
.nav-pane-bottom {
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 3px 3px 5px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { types, validType } from '../consts/support_redis_type.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
validator(value) {
|
||||||
|
return validType(value)
|
||||||
|
},
|
||||||
|
default: 'STRING',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const color = {
|
||||||
|
[types.STRING]: '#626aef',
|
||||||
|
[types.HASH]: '#576bfa',
|
||||||
|
[types.LIST]: '#34b285',
|
||||||
|
[types.SET]: '#bb7d52',
|
||||||
|
[types.ZSET]: '#d053a5',
|
||||||
|
}
|
||||||
|
|
||||||
|
const backgroundColor = computed(() => {
|
||||||
|
return color[props.type]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-tag
|
||||||
|
:bordered="false"
|
||||||
|
:color="{ color: backgroundColor, textColor: 'white' }"
|
||||||
|
:size="props.size"
|
||||||
|
class="redis-type-tag"
|
||||||
|
strong
|
||||||
|
>
|
||||||
|
{{ props.type }}
|
||||||
|
</n-tag>
|
||||||
|
<!-- <div class="redis-type-tag flex-box-h" :style="{backgroundColor: backgroundColor}">{{ props.type }}</div>-->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.redis-type-tag {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-toolbar flex-box-h">
|
||||||
|
<n-button>按钮1</n-button>
|
||||||
|
<n-button>按钮2</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-toolbar {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,288 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, h, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ContentToolbar from '../ContentToolbar.vue'
|
||||||
|
import AddLink from '../icons/AddLink.vue'
|
||||||
|
import { NButton, NCode, NIcon, NInput, useMessage } from 'naive-ui'
|
||||||
|
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
|
||||||
|
import EditableTableColumn from '../EditableTableColumn.vue'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import useDialogStore from '../../stores/dialog.js'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
|
||||||
|
const filterOption = [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: i18n.t('field'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: i18n.t('value'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const filterType = ref(1)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
db: Number,
|
||||||
|
keyPath: String,
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
value: Object,
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
const keyType = redisTypes.HASH
|
||||||
|
const currentEditRow = ref({
|
||||||
|
no: 0,
|
||||||
|
key: '',
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
const fieldColumn = reactive({
|
||||||
|
key: 'key',
|
||||||
|
title: i18n.t('field'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
resizable: true,
|
||||||
|
filterOptionValue: null,
|
||||||
|
filter(value, row) {
|
||||||
|
return !!~row.key.indexOf(value.toString())
|
||||||
|
},
|
||||||
|
// sorter: (row1, row2) => row1.key - row2.key,
|
||||||
|
render: (row) => {
|
||||||
|
const isEdit = currentEditRow.value.no === row.no
|
||||||
|
if (isEdit) {
|
||||||
|
return h(NInput, {
|
||||||
|
value: currentEditRow.value.key,
|
||||||
|
'onUpdate:value': (val) => {
|
||||||
|
currentEditRow.value.key = val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return row.key
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const valueColumn = reactive({
|
||||||
|
key: 'value',
|
||||||
|
title: i18n.t('value'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
resizable: true,
|
||||||
|
filterOptionValue: null,
|
||||||
|
filter(value, row) {
|
||||||
|
return !!~row.value.indexOf(value.toString())
|
||||||
|
},
|
||||||
|
// sorter: (row1, row2) => row1.value - row2.value,
|
||||||
|
// ellipsis: {
|
||||||
|
// tooltip: true
|
||||||
|
// },
|
||||||
|
render: (row) => {
|
||||||
|
const isEdit = currentEditRow.value.no === row.no
|
||||||
|
if (isEdit) {
|
||||||
|
return h(NInput, {
|
||||||
|
value: currentEditRow.value.value,
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: { minRow: 2, maxRows: 5 },
|
||||||
|
style: 'text-align: left;',
|
||||||
|
'onUpdate:value': (val) => {
|
||||||
|
currentEditRow.value.value = val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return h(NCode, { language: 'plaintext', wordWrap: true }, { default: () => row.value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const actionColumn = {
|
||||||
|
key: 'action',
|
||||||
|
title: i18n.t('action'),
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
return h(EditableTableColumn, {
|
||||||
|
editing: currentEditRow.value.no === row.no,
|
||||||
|
bindKey: row.key,
|
||||||
|
onEdit: () => {
|
||||||
|
currentEditRow.value.no = row.no
|
||||||
|
currentEditRow.value.key = row.key
|
||||||
|
currentEditRow.value.value = row.value
|
||||||
|
},
|
||||||
|
onDelete: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.removeHashField(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.key
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('delete_key_succ', { key: row.key }))
|
||||||
|
// update display value
|
||||||
|
// if (!isEmpty(removed)) {
|
||||||
|
// for (const elem of removed) {
|
||||||
|
// delete props.value[elem]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSave: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.setHash(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.key,
|
||||||
|
currentEditRow.value.key,
|
||||||
|
currentEditRow.value.value
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('save_value_succ'))
|
||||||
|
// update display value
|
||||||
|
// if (!isEmpty(updated)) {
|
||||||
|
// for (const key in updated) {
|
||||||
|
// props.value[key] = updated[key]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const columns = reactive([
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
title: '#',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
},
|
||||||
|
fieldColumn,
|
||||||
|
valueColumn,
|
||||||
|
actionColumn,
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableData = computed(() => {
|
||||||
|
const data = []
|
||||||
|
let index = 0
|
||||||
|
for (const key in props.value) {
|
||||||
|
data.push({
|
||||||
|
no: ++index,
|
||||||
|
key,
|
||||||
|
value: props.value[key],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
const message = useMessage()
|
||||||
|
const onAddRow = () => {
|
||||||
|
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, types.HASH)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterValue = ref('')
|
||||||
|
const onFilterInput = (val) => {
|
||||||
|
switch (filterType.value) {
|
||||||
|
case filterOption[0].value:
|
||||||
|
valueColumn.filterOptionValue = null
|
||||||
|
fieldColumn.filterOptionValue = val
|
||||||
|
break
|
||||||
|
case filterOption[1].value:
|
||||||
|
fieldColumn.filterOptionValue = null
|
||||||
|
valueColumn.filterOptionValue = val
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChangeFilterType = (type) => {
|
||||||
|
onFilterInput(filterValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilter = () => {
|
||||||
|
fieldColumn.filterOptionValue = null
|
||||||
|
valueColumn.filterOptionValue = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateFilter = (filters, sourceColumn) => {
|
||||||
|
switch (filterType.value) {
|
||||||
|
case filterOption[0].value:
|
||||||
|
fieldColumn.filterOptionValue = filters[sourceColumn.key]
|
||||||
|
break
|
||||||
|
case filterOption[1].value:
|
||||||
|
valueColumn.filterOptionValue = filters[sourceColumn.key]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-wrapper flex-box-v">
|
||||||
|
<content-toolbar :db="props.db" :key-path="props.keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
|
||||||
|
<div class="tb2 flex-box-h">
|
||||||
|
<div class="flex-box-h">
|
||||||
|
<n-input-group>
|
||||||
|
<n-select
|
||||||
|
v-model:value="filterType"
|
||||||
|
:consistent-menu-width="false"
|
||||||
|
:options="filterOption"
|
||||||
|
style="width: 120px"
|
||||||
|
@update:value="onChangeFilterType"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="filterValue"
|
||||||
|
:placeholder="$t('search')"
|
||||||
|
clearable
|
||||||
|
@clear="clearFilter"
|
||||||
|
@update:value="onFilterInput"
|
||||||
|
/>
|
||||||
|
</n-input-group>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-expand"></div>
|
||||||
|
<n-button plain @click="onAddRow">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="AddLink" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('add_row') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="fill-height flex-box-h" style="user-select: text">
|
||||||
|
<n-data-table
|
||||||
|
:key="(row) => row.no"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:single-column="true"
|
||||||
|
:single-line="false"
|
||||||
|
flex-height
|
||||||
|
max-height="100%"
|
||||||
|
size="small"
|
||||||
|
striped
|
||||||
|
virtual-scroll
|
||||||
|
@update:filters="onUpdateFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,212 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, h, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ContentToolbar from '../ContentToolbar.vue'
|
||||||
|
import AddLink from '../icons/AddLink.vue'
|
||||||
|
import { NButton, NCode, NIcon, NInput, useMessage } from 'naive-ui'
|
||||||
|
import { size } from 'lodash'
|
||||||
|
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
|
||||||
|
import EditableTableColumn from '../EditableTableColumn.vue'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import useDialogStore from '../../stores/dialog.js'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
db: Number,
|
||||||
|
keyPath: String,
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
value: Object,
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
const keyType = redisTypes.LIST
|
||||||
|
const currentEditRow = ref({
|
||||||
|
no: 0,
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
const valueColumn = reactive({
|
||||||
|
key: 'value',
|
||||||
|
title: i18n.t('value'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
filterOptionValue: null,
|
||||||
|
filter(value, row) {
|
||||||
|
return !!~row.value.indexOf(value.toString())
|
||||||
|
},
|
||||||
|
render: (row) => {
|
||||||
|
const isEdit = currentEditRow.value.no === row.no
|
||||||
|
if (isEdit) {
|
||||||
|
return h(NInput, {
|
||||||
|
value: currentEditRow.value.value,
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: { minRow: 2, maxRows: 5 },
|
||||||
|
style: 'text-align: left;',
|
||||||
|
'onUpdate:value': (val) => {
|
||||||
|
currentEditRow.value.value = val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return h(NCode, { language: 'plaintext', wordWrap: true }, { default: () => row.value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const actionColumn = {
|
||||||
|
key: 'action',
|
||||||
|
title: i18n.t('action'),
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
return h(EditableTableColumn, {
|
||||||
|
editing: currentEditRow.value.no === row.no,
|
||||||
|
bindKey: '#' + row.no,
|
||||||
|
onEdit: () => {
|
||||||
|
currentEditRow.value.no = row.no
|
||||||
|
currentEditRow.value.value = row.value
|
||||||
|
},
|
||||||
|
onDelete: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.removeListItem(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.no - 1
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('delete_key_succ', { key: '#' + row.no }))
|
||||||
|
// update display value
|
||||||
|
// if (!isEmpty(removed)) {
|
||||||
|
// props.value.splice(removed[0], 1)
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSave: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.updateListItem(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
currentEditRow.value.no - 1,
|
||||||
|
currentEditRow.value.value
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('save_value_succ'))
|
||||||
|
// update display value
|
||||||
|
// if (!isEmpty(updated)) {
|
||||||
|
// for (const key in updated) {
|
||||||
|
// props.value[key] = updated[key]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const columns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
title: '#',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
},
|
||||||
|
valueColumn,
|
||||||
|
actionColumn,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableData = computed(() => {
|
||||||
|
const data = []
|
||||||
|
const len = size(props.value)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
data.push({
|
||||||
|
no: i + 1,
|
||||||
|
value: props.value[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
const message = useMessage()
|
||||||
|
const onAddValue = (value) => {
|
||||||
|
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, types.LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterValue = ref('')
|
||||||
|
const onFilterInput = (val) => {
|
||||||
|
valueColumn.filterOptionValue = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilter = () => {
|
||||||
|
valueColumn.filterOptionValue = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateFilter = (filters, sourceColumn) => {
|
||||||
|
valueColumn.filterOptionValue = filters[sourceColumn.key]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-wrapper flex-box-v">
|
||||||
|
<content-toolbar :db="props.db" :key-path="props.keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
|
||||||
|
<div class="tb2 flex-box-h">
|
||||||
|
<div class="flex-box-h">
|
||||||
|
<n-input
|
||||||
|
v-model:value="filterValue"
|
||||||
|
:placeholder="$t('search')"
|
||||||
|
clearable
|
||||||
|
@clear="clearFilter"
|
||||||
|
@update:value="onFilterInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-expand"></div>
|
||||||
|
<n-button plain @click="onAddValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="AddLink" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('add_row') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="fill-height flex-box-h" style="user-select: text">
|
||||||
|
<n-data-table
|
||||||
|
:key="(row) => row.no"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:single-column="true"
|
||||||
|
:single-line="false"
|
||||||
|
flex-height
|
||||||
|
max-height="100%"
|
||||||
|
size="small"
|
||||||
|
striped
|
||||||
|
virtual-scroll
|
||||||
|
@update:filters="onUpdateFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,208 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, h, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ContentToolbar from '../ContentToolbar.vue'
|
||||||
|
import AddLink from '../icons/AddLink.vue'
|
||||||
|
import { NButton, NCode, NIcon, NInput, useMessage } from 'naive-ui'
|
||||||
|
import { size } from 'lodash'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import useDialogStore from '../../stores/dialog.js'
|
||||||
|
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
|
||||||
|
import EditableTableColumn from '../EditableTableColumn.vue'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
db: Number,
|
||||||
|
keyPath: String,
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
const keyType = redisTypes.LIST
|
||||||
|
const currentEditRow = ref({
|
||||||
|
no: 0,
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const valueColumn = reactive({
|
||||||
|
key: 'value',
|
||||||
|
title: i18n.t('value'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
filterOptionValue: null,
|
||||||
|
filter(value, row) {
|
||||||
|
return !!~row.value.indexOf(value.toString())
|
||||||
|
},
|
||||||
|
render: (row) => {
|
||||||
|
const isEdit = currentEditRow.value.no === row.no
|
||||||
|
if (isEdit) {
|
||||||
|
return h(NInput, {
|
||||||
|
value: currentEditRow.value.value,
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: { minRow: 2, maxRows: 5 },
|
||||||
|
style: 'text-align: left;',
|
||||||
|
'onUpdate:value': (val) => {
|
||||||
|
currentEditRow.value.value = val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return h(NCode, { language: 'plaintext', wordWrap: true }, { default: () => row.value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const actionColumn = {
|
||||||
|
key: 'action',
|
||||||
|
title: i18n.t('action'),
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
return h(EditableTableColumn, {
|
||||||
|
editing: currentEditRow.value.no === row.no,
|
||||||
|
bindKey: row.value,
|
||||||
|
onEdit: () => {
|
||||||
|
currentEditRow.value.no = row.no
|
||||||
|
currentEditRow.value.key = row.key
|
||||||
|
currentEditRow.value.value = row.value
|
||||||
|
},
|
||||||
|
onDelete: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.removeSetItem(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.value
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('delete_key_succ', { key: row.value }))
|
||||||
|
// update display value
|
||||||
|
// props.value.splice(row.no - 1, 1)
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSave: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.updateSetItem(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.value,
|
||||||
|
currentEditRow.value.value
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('save_value_succ'))
|
||||||
|
// update display value
|
||||||
|
// props.value[row.no - 1] = currentEditRow.value.value
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const columns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
title: '#',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
},
|
||||||
|
valueColumn,
|
||||||
|
actionColumn,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableData = computed(() => {
|
||||||
|
const data = []
|
||||||
|
const len = size(props.value)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
data.push({
|
||||||
|
no: i + 1,
|
||||||
|
value: props.value[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const onAddValue = (value) => {
|
||||||
|
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, types.SET)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterValue = ref('')
|
||||||
|
const onFilterInput = (val) => {
|
||||||
|
valueColumn.filterOptionValue = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilter = () => {
|
||||||
|
valueColumn.filterOptionValue = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateFilter = (filters, sourceColumn) => {
|
||||||
|
valueColumn.filterOptionValue = filters[sourceColumn.key]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-wrapper flex-box-v">
|
||||||
|
<content-toolbar :db="props.db" :key-path="props.keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
|
||||||
|
<div class="tb2 flex-box-h">
|
||||||
|
<div class="flex-box-h">
|
||||||
|
<n-input
|
||||||
|
v-model:value="filterValue"
|
||||||
|
:placeholder="$t('search')"
|
||||||
|
clearable
|
||||||
|
@clear="clearFilter"
|
||||||
|
@update:value="onFilterInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-expand"></div>
|
||||||
|
<n-button plain @click="onAddValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="AddLink" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('add_row') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="fill-height flex-box-h" style="user-select: text">
|
||||||
|
<n-data-table
|
||||||
|
:key="(row) => row.no"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:single-column="true"
|
||||||
|
:single-line="false"
|
||||||
|
flex-height
|
||||||
|
max-height="100%"
|
||||||
|
size="small"
|
||||||
|
striped
|
||||||
|
virtual-scroll
|
||||||
|
@update:filters="onUpdateFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,219 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ContentToolbar from '../ContentToolbar.vue'
|
||||||
|
import Copy from '../icons/Copy.vue'
|
||||||
|
import Save from '../icons/Save.vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { types } from '../../consts/value_view_type.js'
|
||||||
|
import Close from '../icons/Close.vue'
|
||||||
|
import Edit from '../icons/Edit.vue'
|
||||||
|
import { IsJson } from '../../utils/check_string_format.js'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import { types as redisTypes } from '../../consts/support_redis_type.js'
|
||||||
|
import { ClipboardSetText } from '../../../wailsjs/runtime/runtime.js'
|
||||||
|
import { toLower } from 'lodash'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
db: Number,
|
||||||
|
keyPath: String,
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
value: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const viewOption = [
|
||||||
|
{
|
||||||
|
value: types.PLAIN_TEXT,
|
||||||
|
label: types.PLAIN_TEXT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: types.JSON,
|
||||||
|
label: types.JSON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: types.BASE64_TO_TEXT,
|
||||||
|
label: types.BASE64_TO_TEXT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: types.BASE64_TO_JSON,
|
||||||
|
label: types.BASE64_TO_JSON,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const viewAs = ref(types.PLAIN_TEXT)
|
||||||
|
|
||||||
|
const jsonValue = computed(() => {
|
||||||
|
try {
|
||||||
|
const jsonObj = JSON.parse(props.value)
|
||||||
|
return JSON.stringify(jsonObj, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return props.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const autoDetectFormat = () => {
|
||||||
|
// auto check format when loaded
|
||||||
|
if (IsJson(jsonValue.value)) {
|
||||||
|
viewAs.value = types.JSON
|
||||||
|
} else {
|
||||||
|
viewAs.value = types.PLAIN_TEXT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
autoDetectFormat()
|
||||||
|
})
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(value) => {
|
||||||
|
autoDetectFormat()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const keyType = redisTypes.STRING
|
||||||
|
const viewValue = computed(() => {
|
||||||
|
switch (viewAs.value) {
|
||||||
|
case types.PLAIN_TEXT:
|
||||||
|
return props.value
|
||||||
|
case types.JSON:
|
||||||
|
return jsonValue.value
|
||||||
|
case types.BASE64_TO_TEXT:
|
||||||
|
return props.value
|
||||||
|
case types.BASE64_TO_JSON:
|
||||||
|
return props.value
|
||||||
|
default:
|
||||||
|
return props.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const viewLanguage = computed(() => {
|
||||||
|
switch (viewAs.value) {
|
||||||
|
case types.PLAIN_TEXT:
|
||||||
|
case types.BASE64_TO_TEXT:
|
||||||
|
return 'plaintext'
|
||||||
|
case types.JSON:
|
||||||
|
case types.BASE64_TO_JSON:
|
||||||
|
return 'json'
|
||||||
|
default:
|
||||||
|
return 'plaintext'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const i18n = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy value
|
||||||
|
*/
|
||||||
|
const onCopyValue = () => {
|
||||||
|
ClipboardSetText(viewValue.value)
|
||||||
|
.then((succ) => {
|
||||||
|
if (succ) {
|
||||||
|
message.success(i18n.t('copy_succ'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
message.error(e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editValue = ref('')
|
||||||
|
const inEdit = ref(false)
|
||||||
|
const onEditValue = () => {
|
||||||
|
editValue.value = viewValue.value
|
||||||
|
inEdit.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancelEdit = () => {
|
||||||
|
inEdit.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save value
|
||||||
|
*/
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const saving = ref(false)
|
||||||
|
const onSaveValue = async () => {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.setKey(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
toLower(props.keyType),
|
||||||
|
editValue.value,
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
await connectionStore.loadKeyValue(props.name, props.db, props.keyPath)
|
||||||
|
message.success(i18n.t('save_value_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
inEdit.value = false
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-wrapper flex-box-v">
|
||||||
|
<content-toolbar :db="props.db" :key-path="keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
|
||||||
|
<div class="tb2 flex-box-h">
|
||||||
|
<n-text>{{ $t('view_as') }}</n-text>
|
||||||
|
<n-select v-model:value="viewAs" :options="viewOption" style="width: 200px" />
|
||||||
|
<div class="flex-item-expand"></div>
|
||||||
|
<n-button-group v-if="!inEdit">
|
||||||
|
<n-button @click="onCopyValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Copy" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('copy_value') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button plain @click="onEditValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Edit" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('edit_value') }}
|
||||||
|
</n-button>
|
||||||
|
</n-button-group>
|
||||||
|
<n-button-group v-else>
|
||||||
|
<n-button :loading="saving" plain @click="onSaveValue">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Save" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('save_update') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button :loading="saving" plain @click="onCancelEdit">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Close" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('cancel_update') }}
|
||||||
|
</n-button>
|
||||||
|
</n-button-group>
|
||||||
|
</div>
|
||||||
|
<div class="value-wrapper flex-item-expand flex-box-v">
|
||||||
|
<n-scrollbar v-if="!inEdit" class="flex-item-expand">
|
||||||
|
<n-code :code="viewValue" :language="viewLanguage" show-line-numbers word-wrap />
|
||||||
|
</n-scrollbar>
|
||||||
|
<n-input
|
||||||
|
v-else
|
||||||
|
v-model:value="editValue"
|
||||||
|
:disabled="saving"
|
||||||
|
:resizable="false"
|
||||||
|
class="flex-item-expand"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.value-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,238 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, h, reactive, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ContentToolbar from '../ContentToolbar.vue'
|
||||||
|
import AddLink from '../icons/AddLink.vue'
|
||||||
|
import { NButton, NCode, NIcon, NInput, NInputNumber, useMessage } from 'naive-ui'
|
||||||
|
import { types, types as redisTypes } from '../../consts/support_redis_type.js'
|
||||||
|
import EditableTableColumn from '../EditableTableColumn.vue'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import useDialogStore from '../../stores/dialog.js'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
db: Number,
|
||||||
|
keyPath: String,
|
||||||
|
ttl: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
value: Object,
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
const keyType = redisTypes.ZSET
|
||||||
|
const currentEditRow = ref({
|
||||||
|
no: 0,
|
||||||
|
score: 0,
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
const scoreColumn = reactive({
|
||||||
|
key: 'score',
|
||||||
|
title: i18n.t('score'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
resizable: true,
|
||||||
|
render: (row) => {
|
||||||
|
const isEdit = currentEditRow.value.no === row.no
|
||||||
|
if (isEdit) {
|
||||||
|
return h(NInputNumber, {
|
||||||
|
value: currentEditRow.value.score,
|
||||||
|
'onUpdate:value': (val) => {
|
||||||
|
currentEditRow.value.score = val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return row.score
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const valueColumn = reactive({
|
||||||
|
key: 'value',
|
||||||
|
title: i18n.t('value'),
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
resizable: true,
|
||||||
|
filterOptionValue: null,
|
||||||
|
filter(value, row) {
|
||||||
|
return !!~row.value.indexOf(value.toString())
|
||||||
|
},
|
||||||
|
// sorter: (row1, row2) => row1.value - row2.value,
|
||||||
|
// ellipsis: {
|
||||||
|
// tooltip: true
|
||||||
|
// },
|
||||||
|
render: (row) => {
|
||||||
|
const isEdit = currentEditRow.value.no === row.no
|
||||||
|
if (isEdit) {
|
||||||
|
return h(NInput, {
|
||||||
|
value: currentEditRow.value.value,
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: { minRow: 2, maxRows: 5 },
|
||||||
|
style: 'text-align: left;',
|
||||||
|
'onUpdate:value': (val) => {
|
||||||
|
currentEditRow.value.value = val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return h(NCode, { language: 'plaintext', wordWrap: true }, { default: () => row.value })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const actionColumn = {
|
||||||
|
key: 'action',
|
||||||
|
title: i18n.t('action'),
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
return h(EditableTableColumn, {
|
||||||
|
editing: currentEditRow.value.no === row.no,
|
||||||
|
bindKey: row.value,
|
||||||
|
onEdit: () => {
|
||||||
|
currentEditRow.value.no = row.no
|
||||||
|
currentEditRow.value.value = row.value
|
||||||
|
currentEditRow.value.score = row.score
|
||||||
|
},
|
||||||
|
onDelete: async () => {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await connectionStore.removeZSetItem(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.value
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('delete_key_succ', { key: row.value }))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSave: async () => {
|
||||||
|
try {
|
||||||
|
const newValue = currentEditRow.value.value
|
||||||
|
if (isEmpty(newValue)) {
|
||||||
|
message.error(i18n.t('spec_field_required', { key: i18n.t('value') }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { success, msg } = await connectionStore.updateZSetItem(
|
||||||
|
props.name,
|
||||||
|
props.db,
|
||||||
|
props.keyPath,
|
||||||
|
row.value,
|
||||||
|
newValue,
|
||||||
|
currentEditRow.value.score
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
connectionStore.loadKeyValue(props.name, props.db, props.keyPath).then((r) => {})
|
||||||
|
message.success(i18n.t('save_value_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
} finally {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
currentEditRow.value.no = 0
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const columns = reactive([
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
title: '#',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
titleAlign: 'center',
|
||||||
|
},
|
||||||
|
valueColumn,
|
||||||
|
scoreColumn,
|
||||||
|
actionColumn,
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableData = computed(() => {
|
||||||
|
const data = []
|
||||||
|
let index = 0
|
||||||
|
for (const elem of props.value) {
|
||||||
|
data.push({
|
||||||
|
no: ++index,
|
||||||
|
value: elem.value,
|
||||||
|
score: elem.score,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
const message = useMessage()
|
||||||
|
const onAddRow = () => {
|
||||||
|
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, types.ZSET)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterValue = ref('')
|
||||||
|
const onFilterInput = (val) => {
|
||||||
|
valueColumn.filterOptionValue = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChangeFilterType = (type) => {
|
||||||
|
onFilterInput(filterValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilter = () => {
|
||||||
|
valueColumn.filterOptionValue = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateFilter = (filters, sourceColumn) => {
|
||||||
|
valueColumn.filterOptionValue = filters[sourceColumn.key]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="content-wrapper flex-box-v">
|
||||||
|
<content-toolbar :db="props.db" :key-path="props.keyPath" :key-type="keyType" :server="props.name" :ttl="ttl" />
|
||||||
|
<div class="tb2 flex-box-h">
|
||||||
|
<div class="flex-box-h">
|
||||||
|
<n-input
|
||||||
|
v-model:value="filterValue"
|
||||||
|
:placeholder="$t('search')"
|
||||||
|
clearable
|
||||||
|
@clear="clearFilter"
|
||||||
|
@update:value="onFilterInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-expand"></div>
|
||||||
|
<n-button plain @click="onAddRow">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="AddLink" size="18" />
|
||||||
|
</template>
|
||||||
|
{{ $t('add_row') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="fill-height flex-box-h" style="user-select: text">
|
||||||
|
<n-data-table
|
||||||
|
:key="(row) => row.no"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:single-column="true"
|
||||||
|
:single-line="false"
|
||||||
|
flex-height
|
||||||
|
max-height="100%"
|
||||||
|
size="small"
|
||||||
|
striped
|
||||||
|
virtual-scroll
|
||||||
|
@update:filters="onUpdateFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,203 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, watch } from 'vue'
|
||||||
|
import { types } from '../../consts/support_redis_type'
|
||||||
|
import useDialog from '../../stores/dialog'
|
||||||
|
import NewStringValue from '../new_value/NewStringValue.vue'
|
||||||
|
import NewSetValue from '../new_value/NewSetValue.vue'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import AddListValue from '../new_value/AddListValue.vue'
|
||||||
|
import AddHashValue from '../new_value/AddHashValue.vue'
|
||||||
|
import AddZSetValue from '../new_value/AddZSetValue.vue'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const newForm = reactive({
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
key: '',
|
||||||
|
type: '',
|
||||||
|
opType: 0,
|
||||||
|
value: null,
|
||||||
|
reload: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formLabelWidth = '60px'
|
||||||
|
const addValueComponent = {
|
||||||
|
[types.STRING]: NewStringValue,
|
||||||
|
[types.HASH]: AddHashValue,
|
||||||
|
[types.LIST]: AddListValue,
|
||||||
|
[types.SET]: NewSetValue,
|
||||||
|
[types.ZSET]: AddZSetValue,
|
||||||
|
}
|
||||||
|
const defaultValue = {
|
||||||
|
[types.STRING]: '',
|
||||||
|
[types.HASH]: [],
|
||||||
|
[types.LIST]: [],
|
||||||
|
[types.SET]: [],
|
||||||
|
[types.ZSET]: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dialog title
|
||||||
|
* @type {ComputedRef<string>}
|
||||||
|
*/
|
||||||
|
const title = computed(() => {
|
||||||
|
switch (newForm.type) {
|
||||||
|
case types.LIST:
|
||||||
|
return i18n.t('new_item')
|
||||||
|
case types.HASH:
|
||||||
|
return i18n.t('new_field')
|
||||||
|
case types.SET:
|
||||||
|
return i18n.t('new_field')
|
||||||
|
case types.ZSET:
|
||||||
|
return i18n.t('new_field')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
watch(
|
||||||
|
() => dialogStore.addFieldsDialogVisible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
const { server, db, key, type } = dialogStore.addFieldParam
|
||||||
|
newForm.server = server
|
||||||
|
newForm.db = db
|
||||||
|
newForm.key = key
|
||||||
|
newForm.type = type
|
||||||
|
newForm.opType = 0
|
||||||
|
newForm.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const message = useMessage()
|
||||||
|
const onAdd = async () => {
|
||||||
|
try {
|
||||||
|
const { server, db, key, type } = newForm
|
||||||
|
let { value } = newForm
|
||||||
|
if (value == null) {
|
||||||
|
value = defaultValue[type]
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case types.LIST:
|
||||||
|
{
|
||||||
|
let data
|
||||||
|
if (newForm.opType === 1) {
|
||||||
|
data = await connectionStore.prependListItem(server, db, key, value)
|
||||||
|
} else {
|
||||||
|
data = await connectionStore.appendListItem(server, db, key, value)
|
||||||
|
}
|
||||||
|
const { success, msg } = data
|
||||||
|
if (success) {
|
||||||
|
if (newForm.reload) {
|
||||||
|
connectionStore.loadKeyValue(server, db, key).then(() => {})
|
||||||
|
}
|
||||||
|
message.success(i18n.t('handle_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case types.HASH:
|
||||||
|
{
|
||||||
|
const { success, msg } = await connectionStore.addHashField(server, db, key, newForm.opType, value)
|
||||||
|
if (success) {
|
||||||
|
if (newForm.reload) {
|
||||||
|
connectionStore.loadKeyValue(server, db, key).then(() => {})
|
||||||
|
}
|
||||||
|
message.success(i18n.t('handle_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case types.SET:
|
||||||
|
{
|
||||||
|
const { success, msg } = await connectionStore.addSetItem(server, db, key, value)
|
||||||
|
if (success) {
|
||||||
|
if (newForm.reload) {
|
||||||
|
connectionStore.loadKeyValue(server, db, key).then(() => {})
|
||||||
|
}
|
||||||
|
message.success(i18n.t('handle_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case types.ZSET:
|
||||||
|
{
|
||||||
|
const { success, msg } = await connectionStore.addZSetItem(server, db, key, newForm.opType, value)
|
||||||
|
if (success) {
|
||||||
|
if (newForm.reload) {
|
||||||
|
connectionStore.loadKeyValue(server, db, key).then(() => {})
|
||||||
|
}
|
||||||
|
message.success(i18n.t('handle_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dialogStore.closeAddFieldsDialog()
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
dialogStore.closeAddFieldsDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.addFieldsDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:negative-button-props="{ size: 'medium' }"
|
||||||
|
:negative-text="$t('cancel')"
|
||||||
|
:positive-button-props="{ size: 'medium' }"
|
||||||
|
:positive-text="$t('confirm')"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="title"
|
||||||
|
preset="dialog"
|
||||||
|
style="width: 600px"
|
||||||
|
transform-origin="center"
|
||||||
|
@positive-click="onAdd"
|
||||||
|
@negative-click="onClose"
|
||||||
|
>
|
||||||
|
<n-scrollbar style="max-height: 500px">
|
||||||
|
<n-form
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="newForm"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
style="padding-right: 15px"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('key')" path="key" required>
|
||||||
|
<n-input v-model:value="newForm.key" placeholder="" readonly />
|
||||||
|
</n-form-item>
|
||||||
|
<component
|
||||||
|
:is="addValueComponent[newForm.type]"
|
||||||
|
v-model:type="newForm.opType"
|
||||||
|
v-model:value="newForm.value"
|
||||||
|
/>
|
||||||
|
<n-form-item label=" " path="key" required>
|
||||||
|
<n-checkbox v-model:checked="newForm.reload">
|
||||||
|
{{ $t('reload_when_succ') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-scrollbar>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,252 @@
|
||||||
|
<script setup>
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { SaveConnection, TestConnection } from '../../../wailsjs/go/services/connectionService.js'
|
||||||
|
import useDialog from '../../stores/dialog'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import Close from '../icons/Close.vue'
|
||||||
|
|
||||||
|
const generalFormValue = {
|
||||||
|
group: '',
|
||||||
|
name: '',
|
||||||
|
addr: '127.0.0.1',
|
||||||
|
port: 6379,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
defaultFilter: '*',
|
||||||
|
keySeparator: ':',
|
||||||
|
connTimeout: 60,
|
||||||
|
execTimeout: 60,
|
||||||
|
markColor: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const i18n = useI18n()
|
||||||
|
|
||||||
|
const generalForm = ref(Object.assign({}, generalFormValue))
|
||||||
|
const generalFormRules = () => {
|
||||||
|
const requiredMsg = i18n.t('field_required')
|
||||||
|
return {
|
||||||
|
name: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
addr: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
defaultFilter: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
keySeparator: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = ref('general')
|
||||||
|
const testing = ref(false)
|
||||||
|
const showTestResult = ref(false)
|
||||||
|
const testResult = ref('')
|
||||||
|
const showTestConnSuccResult = computed(() => {
|
||||||
|
return isEmpty(testResult.value) && showTestResult.value === true
|
||||||
|
})
|
||||||
|
const showTestConnFailResult = computed(() => {
|
||||||
|
return !isEmpty(testResult.value) && showTestResult.value === true
|
||||||
|
})
|
||||||
|
const formLabelWidth = computed(() => {
|
||||||
|
// Compatible with long english word
|
||||||
|
if (tab.value === 'advanced' && i18n.locale.value === 'en') {
|
||||||
|
return '140px'
|
||||||
|
}
|
||||||
|
return '80px'
|
||||||
|
})
|
||||||
|
const predefineColors = ref(['', '#FE5959', '#FEC230', '#FEF27F', '#6CEFAF', '#46C3FC', '#B388FC', '#B0BEC5'])
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
const generalFormRef = ref(null)
|
||||||
|
const advanceFormRef = ref(null)
|
||||||
|
|
||||||
|
const onCreateConnection = async () => {
|
||||||
|
// Validate general form
|
||||||
|
await generalFormRef.value?.validate((err) => {
|
||||||
|
nextTick(() => (tab.value = 'general'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate advance form
|
||||||
|
await advanceFormRef.value?.validate((err) => {
|
||||||
|
nextTick(() => (tab.value = 'advanced'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Store new connection
|
||||||
|
const { success, msg } = await SaveConnection(generalForm.value, false)
|
||||||
|
if (!success) {
|
||||||
|
message.error(msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(i18n.t('new_conn_succ'))
|
||||||
|
dialogStore.closeNewDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
generalForm.value = generalFormValue
|
||||||
|
generalFormRef.value?.restoreValidation()
|
||||||
|
showTestResult.value = false
|
||||||
|
testResult.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => dialogStore.newDialogVisible,
|
||||||
|
(visible) => {}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTestConnection = async () => {
|
||||||
|
testResult.value = ''
|
||||||
|
testing.value = true
|
||||||
|
let result = ''
|
||||||
|
try {
|
||||||
|
const { addr, port, username, password } = generalForm.value
|
||||||
|
const { success = false, msg } = await TestConnection(addr, port, username, password)
|
||||||
|
if (!success) {
|
||||||
|
result = msg
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
result = e.message
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
showTestResult.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmpty(result)) {
|
||||||
|
testResult.value = result
|
||||||
|
} else {
|
||||||
|
testResult.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
dialogStore.closeNewDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.newDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:on-after-leave="resetForm"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="$t('new_conn_title')"
|
||||||
|
preset="dialog"
|
||||||
|
transform-origin="center"
|
||||||
|
>
|
||||||
|
<n-tabs v-model:value="tab">
|
||||||
|
<n-tab-pane :tab="$t('general')" display-directive="show" name="general">
|
||||||
|
<n-form
|
||||||
|
ref="generalFormRef"
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="generalForm"
|
||||||
|
:rules="generalFormRules()"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('conn_name')" path="name" required>
|
||||||
|
<n-input v-model:value="generalForm.name" :placeholder="$t('conn_name_tip')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_addr')" path="addr" required>
|
||||||
|
<n-input v-model:value="generalForm.addr" :placeholder="$t('conn_addr_tip')" />
|
||||||
|
<n-text style="width: 40px; text-align: center">:</n-text>
|
||||||
|
<n-input-number v-model:value="generalForm.port" :max="65535" :min="1" style="width: 200px" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_pwd')" path="password">
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.password"
|
||||||
|
:placeholder="$t('conn_pwd_tip')"
|
||||||
|
show-password-on="click"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_usr')" path="username">
|
||||||
|
<n-input v-model="generalForm.username" :placeholder="$t('conn_usr_tip')" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane :tab="$t('advanced')" display-directive="show" name="advanced">
|
||||||
|
<n-form
|
||||||
|
ref="advanceFormRef"
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="generalForm"
|
||||||
|
:rules="generalFormRules()"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('conn_advn_filter')" path="defaultFilter">
|
||||||
|
<n-input v-model:value="generalForm.defaultFilter" :placeholder="$t('conn_advn_filter_tip')" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_advn_separator')" path="keySeparator">
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.keySeparator"
|
||||||
|
:placeholder="$t('conn_advn_separator_tip')"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_advn_conn_timeout')" path="connTimeout">
|
||||||
|
<n-input-number v-model:value="generalForm.connTimeout" :max="999999" :min="1">
|
||||||
|
<template #suffix>
|
||||||
|
{{ $t('second') }}
|
||||||
|
</template>
|
||||||
|
</n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_advn_exec_timeout')" path="execTimeout">
|
||||||
|
<n-input-number v-model:value="generalForm.execTimeout" :max="999999" :min="1">
|
||||||
|
<template #suffix>
|
||||||
|
{{ $t('second') }}
|
||||||
|
</template>
|
||||||
|
</n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('conn_advn_mark_color')" path="markColor">
|
||||||
|
<div
|
||||||
|
v-for="color in predefineColors"
|
||||||
|
:key="color"
|
||||||
|
:class="{
|
||||||
|
'color-preset-item_selected': generalForm.markColor === color,
|
||||||
|
}"
|
||||||
|
:style="{ backgroundColor: color }"
|
||||||
|
class="color-preset-item"
|
||||||
|
@click="generalForm.markColor = color"
|
||||||
|
>
|
||||||
|
<n-icon v-if="color === ''" :component="Close" size="24" />
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<!-- test result alert-->
|
||||||
|
<n-alert v-if="showTestConnSuccResult" title="" type="success">
|
||||||
|
{{ $t('conn_test_succ') }}
|
||||||
|
</n-alert>
|
||||||
|
<n-alert v-if="showTestConnFailResult" title="" type="error">
|
||||||
|
{{ $t('conn_test_fail') }}: {{ testResult }}
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
<template #action>
|
||||||
|
<div class="flex-item-expand">
|
||||||
|
<n-button :loading="testing" @click="onTestConnection">{{ $t('conn_test') }}</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item n-dialog__action">
|
||||||
|
<n-button @click="onClose">{{ $t('cancel') }}</n-button>
|
||||||
|
<n-button type="primary" @click="onCreateConnection">{{ $t('confirm') }}</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.color-preset-item {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 2px;
|
||||||
|
border: white 3px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&_selected,
|
||||||
|
&:hover {
|
||||||
|
border-color: #cdd0d6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,147 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import { types } from '../../consts/support_redis_type'
|
||||||
|
import useDialog from '../../stores/dialog'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import NewStringValue from '../new_value/NewStringValue.vue'
|
||||||
|
import NewHashValue from '../new_value/NewHashValue.vue'
|
||||||
|
import NewListValue from '../new_value/NewListValue.vue'
|
||||||
|
import NewZSetValue from '../new_value/NewZSetValue.vue'
|
||||||
|
import NewSetValue from '../new_value/NewSetValue.vue'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const newForm = reactive({
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
key: '',
|
||||||
|
type: '',
|
||||||
|
ttl: -1,
|
||||||
|
value: null,
|
||||||
|
})
|
||||||
|
const formRules = computed(() => {
|
||||||
|
const requiredMsg = i18n.t('field_required')
|
||||||
|
return {
|
||||||
|
key: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
type: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
ttl: { required: true, message: requiredMsg, trigger: 'input' },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const newFormRef = ref(null)
|
||||||
|
|
||||||
|
const formLabelWidth = '60px'
|
||||||
|
const options = computed(() => {
|
||||||
|
return Object.keys(types).map((t) => ({
|
||||||
|
value: t,
|
||||||
|
label: t,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
const addValueComponent = {
|
||||||
|
[types.STRING]: NewStringValue,
|
||||||
|
[types.HASH]: NewHashValue,
|
||||||
|
[types.LIST]: NewListValue,
|
||||||
|
[types.SET]: NewSetValue,
|
||||||
|
[types.ZSET]: NewZSetValue,
|
||||||
|
}
|
||||||
|
const defaultValue = {
|
||||||
|
[types.STRING]: '',
|
||||||
|
[types.HASH]: [],
|
||||||
|
[types.LIST]: [],
|
||||||
|
[types.SET]: [],
|
||||||
|
[types.ZSET]: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
watch(
|
||||||
|
() => dialogStore.newKeyDialogVisible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
const { prefix, server, db } = dialogStore.newKeyParam
|
||||||
|
newForm.server = server
|
||||||
|
newForm.db = db
|
||||||
|
newForm.key = isEmpty(prefix) ? '' : prefix
|
||||||
|
newForm.type = options.value[0].value
|
||||||
|
newForm.ttl = -1
|
||||||
|
newForm.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const onAdd = async () => {
|
||||||
|
await newFormRef.value?.validate()
|
||||||
|
try {
|
||||||
|
const { server, db, key, type, ttl } = newForm
|
||||||
|
let { value } = newForm
|
||||||
|
if (value == null) {
|
||||||
|
value = defaultValue[type]
|
||||||
|
}
|
||||||
|
await connectionStore.setKey(server, db, key, type, value, ttl)
|
||||||
|
dialogStore.closeNewKeyDialog()
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
dialogStore.closeNewKeyDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.newKeyDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:negative-button-props="{ size: 'medium' }"
|
||||||
|
:negative-text="$t('cancel')"
|
||||||
|
:positive-button-props="{ size: 'medium' }"
|
||||||
|
:positive-text="$t('confirm')"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="$t('new_key')"
|
||||||
|
preset="dialog"
|
||||||
|
style="width: 600px"
|
||||||
|
transform-origin="center"
|
||||||
|
@positive-click="onAdd"
|
||||||
|
@negative-click="onClose"
|
||||||
|
>
|
||||||
|
<n-scrollbar style="max-height: 500px">
|
||||||
|
<n-form
|
||||||
|
ref="newFormRef"
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="newForm"
|
||||||
|
:rules="formRules"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
style="padding-right: 15px"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('key')" path="key" required>
|
||||||
|
<n-input v-model:value="newForm.key" placeholder="" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('type')" path="type" required>
|
||||||
|
<n-select v-model:value="newForm.type" :options="options" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('ttl')" required>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="newForm.ttl"
|
||||||
|
:max="Number.MAX_SAFE_INTEGER"
|
||||||
|
:min="-1"
|
||||||
|
placeholder="TTL"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
{{ $t('second') }}
|
||||||
|
</template>
|
||||||
|
</n-input-number>
|
||||||
|
<n-button secondary type="primary" @click="newForm.ttl = -1">{{ $t('persist_key') }}</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
<component :is="addValueComponent[newForm.type]" v-model:value="newForm.value" />
|
||||||
|
<!-- TODO: Add import from txt file option -->
|
||||||
|
</n-form>
|
||||||
|
</n-scrollbar>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,189 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { GetPreferences, RestoreDefault, SetPreferencesN } from '../../../wailsjs/go/storage/PreferencesStorage.js'
|
||||||
|
import { lang } from '../../langs/index'
|
||||||
|
import useDialog from '../../stores/dialog'
|
||||||
|
|
||||||
|
const langOption = Object.entries(lang).map(([key, value]) => ({
|
||||||
|
value: key,
|
||||||
|
label: `${value['lang_name']}`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const fontOption = [
|
||||||
|
{
|
||||||
|
label: 'JetBrains Mono',
|
||||||
|
value: 'JetBrains Mono',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const generalForm = reactive({
|
||||||
|
language: langOption[0].value,
|
||||||
|
font: '',
|
||||||
|
fontSize: 14,
|
||||||
|
useSystemProxy: false,
|
||||||
|
useSystemProxyHttp: false,
|
||||||
|
checkUpdate: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const editorForm = reactive({
|
||||||
|
font: '',
|
||||||
|
fontSize: 14,
|
||||||
|
})
|
||||||
|
const prevPreferences = ref({})
|
||||||
|
const tab = ref('general')
|
||||||
|
const formLabelWidth = '80px'
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
const i18n = useI18n()
|
||||||
|
|
||||||
|
const applyPreferences = (pf) => {
|
||||||
|
const { general = {}, editor = {} } = pf
|
||||||
|
generalForm.language = general['language']
|
||||||
|
generalForm.font = general['font']
|
||||||
|
generalForm.fontSize = general['font_size'] || 14
|
||||||
|
generalForm.useSystemProxy = general['use_system_proxy'] === true
|
||||||
|
generalForm.useSystemProxyHttp = general['use_system_proxy_http'] === true
|
||||||
|
generalForm.checkUpdate = general['check_update'] === true
|
||||||
|
|
||||||
|
editorForm.font = editor['font']
|
||||||
|
editorForm.fontSize = editor['font_size'] || 14
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => dialogStore.preferencesDialogVisible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
GetPreferences()
|
||||||
|
.then((pf) => {
|
||||||
|
// load preferences from local
|
||||||
|
applyPreferences(pf)
|
||||||
|
prevPreferences.value = pf
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onSavePreferences = async () => {
|
||||||
|
const pf = {
|
||||||
|
'general.language': generalForm.language,
|
||||||
|
'general.font': generalForm.font,
|
||||||
|
'general.font_size': generalForm.fontSize,
|
||||||
|
'general.use_system_proxy': generalForm.useSystemProxy,
|
||||||
|
'general.use_system_proxy_http': generalForm.useSystemProxyHttp,
|
||||||
|
'general.check_update': generalForm.checkUpdate,
|
||||||
|
|
||||||
|
'editor.font': editorForm.font,
|
||||||
|
'editor.font_size': editorForm.fontSize,
|
||||||
|
}
|
||||||
|
await SetPreferencesN(pf)
|
||||||
|
dialogStore.closePreferencesDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch language and dynamically switch
|
||||||
|
watch(
|
||||||
|
() => generalForm.language,
|
||||||
|
(lang) => (i18n.locale.value = lang)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => generalForm.font,
|
||||||
|
(font) => {}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onRestoreDefaults = async () => {
|
||||||
|
const pf = await RestoreDefault()
|
||||||
|
applyPreferences(pf)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
dialogStore.closePreferencesDialog()
|
||||||
|
// restore to old preferences
|
||||||
|
applyPreferences(prevPreferences.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.preferencesDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="$t('preferences')"
|
||||||
|
preset="dialog"
|
||||||
|
transform-origin="center"
|
||||||
|
>
|
||||||
|
<n-tabs v-model:value="tab">
|
||||||
|
<n-tab-pane :tab="$t('general')" display-directive="show" name="general">
|
||||||
|
<n-form
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="generalForm"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('language')" required>
|
||||||
|
<n-select v-model:value="generalForm.language" :options="langOption" filterable />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('font')" required>
|
||||||
|
<n-select v-model:value="generalForm.font" :options="fontOption" filterable />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('font_size')">
|
||||||
|
<n-input-number v-model:value="generalForm.fontSize" :max="65535" :min="1" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('proxy')">
|
||||||
|
<n-space>
|
||||||
|
<n-checkbox v-model:checked="generalForm.useSystemProxy">
|
||||||
|
{{ $t('use_system_proxy') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-checkbox v-model:checked="generalForm.useSystemProxyHttp">
|
||||||
|
{{ $t('use_system_proxy_http') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-space>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('update')">
|
||||||
|
<n-checkbox v-model:checked="generalForm.checkUpdate"
|
||||||
|
>{{ $t('auto_check_update') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane :tab="$t('editor')" display-directive="show" name="editor">
|
||||||
|
<n-form
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="editorForm"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('font')" :label-width="formLabelWidth" required>
|
||||||
|
<n-select v-model="editorForm.font" :options="fontOption" filterable />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('font_size')" :label-width="formLabelWidth">
|
||||||
|
<n-input-number v-model="editorForm.fontSize" :max="65535" :min="1" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<template #action>
|
||||||
|
<div class="flex-item-expand">
|
||||||
|
<n-button @click="onRestoreDefaults">{{ $t('restore_defaults') }}</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item n-dialog__action">
|
||||||
|
<n-button @click="onClose">{{ $t('cancel') }}</n-button>
|
||||||
|
<n-button type="primary" @click="onSavePreferences">{{ $t('save') }}</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.inline-form-item {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import useDialog from '../../stores/dialog'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const renameForm = reactive({
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
key: '',
|
||||||
|
newKey: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
watch(
|
||||||
|
() => dialogStore.renameDialogVisible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
const { server, db, key } = dialogStore.renameKeyParam
|
||||||
|
renameForm.server = server
|
||||||
|
renameForm.db = db
|
||||||
|
renameForm.key = key
|
||||||
|
renameForm.newKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
const onRename = async () => {
|
||||||
|
try {
|
||||||
|
const { server, db, key, newKey } = renameForm
|
||||||
|
const { success, msg } = await connectionStore.renameKey(server, db, key, newKey)
|
||||||
|
if (success) {
|
||||||
|
await connectionStore.loadKeyValue(server, db, newKey)
|
||||||
|
message.success(i18n.t('handle_succ'))
|
||||||
|
} else {
|
||||||
|
message.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message)
|
||||||
|
}
|
||||||
|
dialogStore.closeRenameKeyDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
dialogStore.closeRenameKeyDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.renameDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:negative-button-props="{ size: 'medium' }"
|
||||||
|
:negative-text="$t('cancel')"
|
||||||
|
:positive-button-props="{ size: 'medium' }"
|
||||||
|
:positive-text="$t('confirm')"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="$t('rename_key')"
|
||||||
|
preset="dialog"
|
||||||
|
transform-origin="center"
|
||||||
|
@positive-click="onRename"
|
||||||
|
@negative-click="onClose"
|
||||||
|
>
|
||||||
|
<n-form
|
||||||
|
:model="renameForm"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="left"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="auto"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('new_key_name')" required>
|
||||||
|
<n-input v-model:value="renameForm.newKey" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import useDialog from '../../stores/dialog'
|
||||||
|
import useTabStore from '../../stores/tab.js'
|
||||||
|
import useConnectionStore from '../../stores/connection.js'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
const ttlForm = reactive({
|
||||||
|
ttl: -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formLabelWidth = '80px'
|
||||||
|
const dialogStore = useDialog()
|
||||||
|
const connectionStore = useConnectionStore()
|
||||||
|
const tabStore = useTabStore()
|
||||||
|
|
||||||
|
const currentServer = ref('')
|
||||||
|
const currentKey = ref('')
|
||||||
|
const currentDB = ref(0)
|
||||||
|
watch(
|
||||||
|
() => dialogStore.ttlDialogVisible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
// get ttl from current tab
|
||||||
|
const tab = tabStore.currentTab
|
||||||
|
if (tab != null) {
|
||||||
|
ttlForm.key = tab.key
|
||||||
|
if (tab.ttl < 0) {
|
||||||
|
// forever
|
||||||
|
} else {
|
||||||
|
ttlForm.ttl = tab.ttl
|
||||||
|
}
|
||||||
|
currentServer.value = tab.name
|
||||||
|
currentDB.value = tab.db
|
||||||
|
currentKey.value = tab.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
dialogStore.closeTTLDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
const tab = tabStore.currentTab
|
||||||
|
if (tab == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const success = await connectionStore.setTTL(tab.name, tab.db, tab.key, ttlForm.ttl)
|
||||||
|
if (success) {
|
||||||
|
tabStore.updateTTL({
|
||||||
|
server: currentServer.value,
|
||||||
|
db: currentDB.value,
|
||||||
|
key: currentKey.value,
|
||||||
|
ttl: ttlForm.ttl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
dialogStore.closeTTLDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="dialogStore.ttlDialogVisible"
|
||||||
|
:closable="false"
|
||||||
|
:close-on-esc="false"
|
||||||
|
:mask-closable="false"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="$t('set_ttl')"
|
||||||
|
preset="dialog"
|
||||||
|
transform-origin="center"
|
||||||
|
>
|
||||||
|
<n-form
|
||||||
|
:label-width="formLabelWidth"
|
||||||
|
:model="ttlForm"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-align="right"
|
||||||
|
label-placement="left"
|
||||||
|
>
|
||||||
|
<n-form-item :label="$t('key')">
|
||||||
|
<n-input :value="currentKey" readonly />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('ttl')" required>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="ttlForm.ttl"
|
||||||
|
:max="Number.MAX_SAFE_INTEGER"
|
||||||
|
:min="-1"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
{{ $t('second') }}
|
||||||
|
</template>
|
||||||
|
</n-input-number>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
|
<template #action>
|
||||||
|
<div class="flex-item-expand">
|
||||||
|
<n-button @click="ttlForm.ttl = -1">{{ $t('persist_key') }}</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item n-dialog__action">
|
||||||
|
<n-button @click="onClose">{{ $t('cancel') }}</n-button>
|
||||||
|
<n-button type="primary" @click="onConfirm">{{ $t('save') }}</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 16V32"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M16 24L32 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M5 8C5 6.89543 5.89543 6 7 6H19L24 12H41C42.1046 12 43 12.8954 43 14V40C43 41.1046 42.1046 42 41 42H7C5.89543 42 5 41.1046 5 40V8Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M18 27H30" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M24 21L24 33" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M8 28H24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M8 37H24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M8 19H40"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M8 10H40"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M30 33H40"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M35 28L35 38"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M29.6567 18.3432L18.343 29.6569"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M18.3433 18.3432L29.657 29.6569"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 4L18 10H10V18L4 24L10 30V38H18L24 44L30 38H38V30L44 24L38 18V10H30L24 4Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 30C27.3137 30 30 27.3137 30 24C30 20.6863 27.3137 18 24 18C20.6863 18 18 20.6863 18 24C18 27.3137 20.6863 30 24 30Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" height="24" viewBox="0 0 48 48" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M30 19H20C15.5817 19 12 22.5817 12 27C12 31.4183 15.5817 35 20 35H36C40.4183 35 44 31.4183 44 27C44 24.9711 43.2447 23.1186 42 21.7084"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M6 24.2916C4.75527 22.8814 4 21.0289 4 19C4 14.5817 7.58172 11 12 11H28C32.4183 11 36 14.5817 36 19C36 23.4183 32.4183 27 28 27H18"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M13 38H41V16H30V4H13V38Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M30 4L41 16"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M7 20V44H28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M19 20H23" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M19 28H31" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M12 9.92704V7C12 5.34315 13.3431 4 15 4H41C42.6569 4 44 5.34315 44 7V33C44 34.6569 42.6569 36 41 36H38.0174"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
fill="none"
|
||||||
|
height="34"
|
||||||
|
rx="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
width="34"
|
||||||
|
x="4"
|
||||||
|
y="10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M18.4394 23.1101L23.7319 17.6006C25.1835 16.1489 27.5691 16.1809 29.0602 17.672C30.5513 19.1631 30.5833 21.5487 29.1316 23.0003L27.2215 25.0231"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M13.4661 28.7472C12.9558 29.2575 11.9006 30.2765 11.9006 30.2765C10.4489 31.7281 10.4095 34.3155 11.9006 35.8066C13.3917 37.2977 15.7772 37.3296 17.2289 35.878L22.3931 31.1896"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M18.6626 28.3284C17.97 27.6358 17.5922 26.7502 17.5317 25.8548C17.4619 24.8226 17.8138 23.7774 18.5912 23.0001"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M22.3213 25.8613C23.8124 27.3524 23.8444 29.738 22.3927 31.1896"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M15 12L16.2 5H31.8L33 12"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M6 12H42" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M37 12L35 43H13L11 12H37Z"
|
||||||
|
fill="none"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M20 22V34" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M28 22V34" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M7 42H43"
|
||||||
|
stroke="currentColorr"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M11 26.7199V34H18.3172L39 13.3081L31.6951 6L11 26.7199Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M6 9L20.4 25.8178V38.4444L27.6 42V25.8178L42 9H6Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#f2c55c',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:fill="props.fillColor"
|
||||||
|
d="M218.1 167.17c0 13 0 25.6 4.1 37.4c-43.1 50.6-156.9 184.3-167.5 194.5a20.17 20.17 0 0 0-6.7 15c0 8.5 5.2 16.7 9.6 21.3c6.6 6.9 34.8 33 40 28c15.4-15 18.5-19 24.8-25.2c9.5-9.3-1-28.3 2.3-36s6.8-9.2 12.5-10.4s15.8 2.9 23.7 3c8.3.1 12.8-3.4 19-9.2c5-4.6 8.6-8.9 8.7-15.6c.2-9-12.8-20.9-3.1-30.4s23.7 6.2 34 5s22.8-15.5 24.1-21.6s-11.7-21.8-9.7-30.7c.7-3 6.8-10 11.4-11s25 6.9 29.6 5.9c5.6-1.2 12.1-7.1 17.4-10.4c15.5 6.7 29.6 9.4 47.7 9.4c68.5 0 124-53.4 124-119.2S408.5 48 340 48s-121.9 53.37-121.9 119.17zM400 144a32 32 0 1 1-32-32a32 32 0 0 1 32 32z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="32"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<!-- <svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">-->
|
||||||
|
<!-- <path-->
|
||||||
|
<!-- d="m38.3682,9.27721c2.5423,2.4953 3.5456,6.1544 2.6289,9.5881c-0.9166,3.4337 -3.6127,6.1156 -7.0646,7.0274c-3.4519,0.9119 -7.1303,-0.0861 -9.63875,-2.6151c-3.77167,-3.8845 -3.71773,-10.0592 0.12124,-13.878c3.83901,-3.8187 10.04631,-3.8724 13.95141,-0.1206l0.0018,-0.0018z"-->
|
||||||
|
<!-- :fill="props.fillColor" stroke="currentColor" :stroke-width="props.strokeWidth" stroke-linejoin="round"/>-->
|
||||||
|
<!-- <path d="m6.5,40.5l17,-17" stroke="currentColor" :stroke-width="props.strokeWidth" stroke-linecap="round"-->
|
||||||
|
<!-- stroke-linejoin="round"/>-->
|
||||||
|
<!-- <path d="m10.5,36.8l5.4285,5.4l6.3334,-6.3l-5.4286,-5.4l-6.3333,6.3z" :fill="props.fillColor"-->
|
||||||
|
<!-- stroke="currentColor" :stroke-width="props.strokeWidth" stroke-linejoin="round"/>-->
|
||||||
|
<!-- </svg>-->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#f2c55c',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:fill="props.fillColor"
|
||||||
|
d="M4 11.9143L24 19L44 11.9143L24 5L4 11.9143Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M4 20L24 27L44 20"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M4 28L24 35L44 28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M4 36L24 43L44 36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'currentColor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M36.7279 36.7279C33.4706 39.9853 28.9706 42 24 42C14.0589 42 6 33.9411 6 24C6 14.0589 14.0589 6 24 6C28.9706 6 33.4706 8.01472 36.7279 11.2721C38.3859 12.9301 42 17 42 17"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
class="default-stroke"
|
||||||
|
d="M42 8V17H33"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M39.3 6H8.7C7.20883 6 6 7.20883 6 8.7V39.3C6 40.7912 7.20883 42 8.7 42H39.3C40.7912 42 42 40.7912 42 39.3V8.7C42 7.20883 40.7912 6 39.3 6Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M32 6V24H15V6H32Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M26 13V17" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M10.9971 6H35.9986" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M21 38C30.3888 38 38 30.3888 38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M26.657 14.3431C25.2093 12.8954 23.2093 12 21.0001 12C18.791 12 16.791 12.8954 15.3433 14.3431"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M33.2216 33.2217L41.7069 41.707"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M25 14L16 5L7 14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M15.9917 31V5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M42 34L33 43L24 34"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M32.9917 17V43"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle :stroke-width="props.strokeWidth" cx="24" cy="28" fill="none" r="16" stroke="currentColor" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M28 4L20 4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 4V12"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M35 16L38 13"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 28V22"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M24 28H18"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,101 @@
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
clickToggle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#dc423c',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onToggle = () => {
|
||||||
|
if (props.clickToggle) {
|
||||||
|
emit('update:modelValue', !props.modelValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="props.modelValue" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44.0001 11C44.0001 11 44 36.0623 44 38C44 41.3137 35.0457 44 24 44C12.9543 44 4.00003 41.3137 4.00003 38C4.00003 36.1423 4 11 4 11"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44 29C44 32.3137 35.0457 35 24 35C12.9543 35 4 32.3137 4 29"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44 20C44 23.3137 35.0457 26 24 26C12.9543 26 4 23.3137 4 20"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
cx="24"
|
||||||
|
cy="10"
|
||||||
|
fill="#dc423c"
|
||||||
|
rx="20"
|
||||||
|
ry="6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44.0001 11C44.0001 11 44 36.0623 44 38C44 41.3137 35.0457 44 24 44C12.9543 44 4.00003 41.3137 4.00003 38C4.00003 36.1423 4 11 4 11"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44 29C44 32.3137 35.0457 35 24 35C12.9543 35 4 32.3137 4 29"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44 20C44 23.3137 35.0457 26 24 26C12.9543 26 4 23.3137 4 20"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
cx="24"
|
||||||
|
cy="10"
|
||||||
|
fill="none"
|
||||||
|
rx="20"
|
||||||
|
ry="6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#ffce78',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="props.modelValue" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M4 9V41L9 21H39.5V15C39.5 13.8954 38.6046 13 37.5 13H24L19 7H6C4.89543 7 4 7.89543 4 9Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:fill="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M40 41L44 21H8.8125L4 41H40Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:fill="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M5 8C5 6.89543 5.89543 6 7 6H19L24 12H41C42.1046 12 43 12.8954 43 14V40C43 41.1046 42.1046 42 41 42H7C5.89543 42 5 41.1046 5 40V8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M43 22H5" stroke="currentColor" stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M5 16V28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M43 16V28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
clickToggle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
fillColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#dc423c',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onToggle = () => {
|
||||||
|
if (props.clickToggle) {
|
||||||
|
emit('update:modelValue', !props.modelValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
v-if="props.modelValue"
|
||||||
|
:height="props.size"
|
||||||
|
:width="props.size"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M41 4H7C5.34315 4 4 5.34315 4 7V41C4 42.6569 5.34315 44 7 44H41C42.6569 44 44 42.6569 44 41V7C44 5.34315 42.6569 4 41 4Z"
|
||||||
|
fill="#dc423c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M4 32H44" stroke="#FFF" stroke-linecap="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M10 38H11"
|
||||||
|
stroke="#FFF"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M26 38H38"
|
||||||
|
stroke="#FFF"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44 37V27"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke="props.fillColor"
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M4 37V27"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
:height="props.size"
|
||||||
|
:width="props.size"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M41 4H7C5.34315 4 4 5.34315 4 7V41C4 42.6569 5.34315 44 7 44H41C42.6569 44 44 42.6569 44 41V7C44 5.34315 42.6569 4 41 4Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path :stroke-width="props.strokeWidth" d="M4 32H44" stroke="currentColor" stroke-linecap="round" />
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M10 38H11"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M26 38H38"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M44 37V27"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="props.strokeWidth"
|
||||||
|
d="M4 37V27"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
strokeWidth: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M42 6L37 11"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M11 25.9999L14 22.9999L25 33.9999L22 36.9999C20.5 38.5 15 41 11 36.9999C7 32.9999 9.5 27.5 11 25.9999Z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M23 32L27 28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M6 42L11 37"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:stroke-width="strokeWidth"
|
||||||
|
d="M16 25L20 21"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { flatMap, reject } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: Number,
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value', 'update:type'])
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const updateOption = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: i18n.t('overwrite_field'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: i18n.t('ignore_field'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Hash
|
||||||
|
* @property {string} key
|
||||||
|
* @property {string} [value]
|
||||||
|
*/
|
||||||
|
const kvList = ref([{ key: '', value: '' }])
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Hash[]} val
|
||||||
|
*/
|
||||||
|
const onUpdate = (val) => {
|
||||||
|
val = reject(val, { key: '' })
|
||||||
|
emit(
|
||||||
|
'update:value',
|
||||||
|
flatMap(val, (item) => [item.key, item.value])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('type')">
|
||||||
|
<n-radio-group :value="props.type" @update:value="(val) => emit('update:type', val)">
|
||||||
|
<n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input
|
||||||
|
v-model:value="kvList"
|
||||||
|
:key-placeholder="$t('enter_field')"
|
||||||
|
:value-placeholder="$t('enter_value')"
|
||||||
|
preset="pair"
|
||||||
|
@update:value="onUpdate"
|
||||||
|
>
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="kvList.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { compact } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: Number,
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value', 'update:type'])
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const insertOption = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: i18n.t('append_item'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: i18n.t('prepend_item'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const list = ref([''])
|
||||||
|
const onUpdate = (val) => {
|
||||||
|
val = compact(val)
|
||||||
|
emit('update:value', val)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('type')">
|
||||||
|
<n-radio-group :value="props.type" @update:value="(val) => emit('update:type', val)">
|
||||||
|
<n-radio-button v-for="(op, i) in insertOption" :key="i" :label="op.label" :value="op.value" />
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input v-model:value="list" :placeholder="$t('enter_elem')" @update:value="onUpdate">
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="list.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,77 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { isEmpty, reject } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: Number,
|
||||||
|
value: Object,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value', 'update:type'])
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
const updateOption = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: i18n.t('overwrite_field'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: i18n.t('ignore_field'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ZSetItem
|
||||||
|
* @property {string} value
|
||||||
|
* @property {string} score
|
||||||
|
*/
|
||||||
|
const zset = ref([{ value: '', score: 0 }])
|
||||||
|
const onCreate = () => {
|
||||||
|
return {
|
||||||
|
value: '',
|
||||||
|
score: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* update input items
|
||||||
|
*/
|
||||||
|
const onUpdate = () => {
|
||||||
|
const val = reject(zset.value, (v) => v == null || isEmpty(v.value))
|
||||||
|
const result = {}
|
||||||
|
for (const elem of val) {
|
||||||
|
result[elem.value] = elem.score
|
||||||
|
}
|
||||||
|
emit('update:value', result)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('type')">
|
||||||
|
<n-radio-group :value="props.type" @update:value="(val) => emit('update:type', val)">
|
||||||
|
<n-radio-button v-for="(op, i) in updateOption" :key="i" :label="op.label" :value="op.value" />
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate">
|
||||||
|
<template #default="{ value }">
|
||||||
|
<n-input
|
||||||
|
v-model:value="value.value"
|
||||||
|
:placeholder="$t('enter_elem')"
|
||||||
|
type="text"
|
||||||
|
@update:value="onUpdate"
|
||||||
|
/>
|
||||||
|
<n-input-number v-model:value="value.score" :placeholder="$t('enter_score')" @update:value="onUpdate" />
|
||||||
|
</template>
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="zset.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { flatMap, reject } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Hash
|
||||||
|
* @property {string} key
|
||||||
|
* @property {string} [value]
|
||||||
|
*/
|
||||||
|
const kvList = ref([{ key: '', value: '' }])
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Hash[]} val
|
||||||
|
*/
|
||||||
|
const onUpdate = (val) => {
|
||||||
|
val = reject(val, { key: '' })
|
||||||
|
emit(
|
||||||
|
'update:value',
|
||||||
|
flatMap(val, (item) => [item.key, item.value])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input
|
||||||
|
v-model:value="kvList"
|
||||||
|
:key-placeholder="$t('enter_field')"
|
||||||
|
:value-placeholder="$t('enter_value')"
|
||||||
|
preset="pair"
|
||||||
|
@update:value="onUpdate"
|
||||||
|
>
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="kvList.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { compact } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
const list = ref([''])
|
||||||
|
const onUpdate = (val) => {
|
||||||
|
val = compact(val)
|
||||||
|
emit('update:value', val)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input v-model:value="list" :placeholder="$t('enter_elem')" @update:value="onUpdate">
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="list.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { compact, uniq } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
const set = ref([''])
|
||||||
|
const onUpdate = (val) => {
|
||||||
|
val = uniq(compact(val))
|
||||||
|
emit('update:value', val)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input v-model:value="set" :placeholder="$t('enter_elem')" @update:value="onUpdate">
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="set.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
value: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('value')">
|
||||||
|
<n-input
|
||||||
|
:rows="6"
|
||||||
|
:value="props.value"
|
||||||
|
placeholder=""
|
||||||
|
type="textarea"
|
||||||
|
@input="(val) => emit('update:value', val)"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { flatMap, isEmpty, reject } from 'lodash'
|
||||||
|
import Add from '../icons/Add.vue'
|
||||||
|
import Delete from '../icons/Delete.vue'
|
||||||
|
import IconButton from '../IconButton.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: Array,
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:value'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ZSetItem
|
||||||
|
* @property {string} value
|
||||||
|
* @property {string} score
|
||||||
|
*/
|
||||||
|
const zset = ref([{ value: '', score: 0 }])
|
||||||
|
const onCreate = () => {
|
||||||
|
return {
|
||||||
|
value: '',
|
||||||
|
score: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* update input items
|
||||||
|
*/
|
||||||
|
const onUpdate = () => {
|
||||||
|
const val = reject(zset.value, (v) => v == null || isEmpty(v.value))
|
||||||
|
emit(
|
||||||
|
'update:value',
|
||||||
|
flatMap(val, (item) => [item.value, item.score])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-form-item :label="$t('element')" required>
|
||||||
|
<n-dynamic-input v-model:value="zset" @create="onCreate" @update:value="onUpdate">
|
||||||
|
<template #default="{ value }">
|
||||||
|
<n-input
|
||||||
|
v-model:value="value.value"
|
||||||
|
:placeholder="$t('enter_member')"
|
||||||
|
type="text"
|
||||||
|
@update:value="onUpdate"
|
||||||
|
/>
|
||||||
|
<n-input-number v-model:value="value.score" :placeholder="$t('enter_score')" @update:value="onUpdate" />
|
||||||
|
</template>
|
||||||
|
<template #action="{ index, create, remove, move }">
|
||||||
|
<icon-button v-if="zset.length > 1" :icon="Delete" size="18" @click="() => remove(index)" />
|
||||||
|
<icon-button :icon="Add" size="18" @click="() => create(index)" />
|
||||||
|
</template>
|
||||||
|
</n-dynamic-input>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const ConnectionType = {
|
||||||
|
Group: 0,
|
||||||
|
Server: 1,
|
||||||
|
RedisDB: 2,
|
||||||
|
RedisKey: 3,
|
||||||
|
RedisValue: 4,
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const types = {
|
||||||
|
STRING: 'STRING',
|
||||||
|
HASH: 'HASH',
|
||||||
|
LIST: 'LIST',
|
||||||
|
SET: 'SET',
|
||||||
|
ZSET: 'ZSET',
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name]))
|
||||||
|
|
||||||
|
export const validType = (t) => {
|
||||||
|
return types.hasOwnProperty(t)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ConnectionType } from './connection_type.js'
|
||||||
|
|
||||||
|
const i18n = useI18n()
|
||||||
|
export const contextMenuKey = {
|
||||||
|
[ConnectionType.Server]: {
|
||||||
|
key: '',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const types = {
|
||||||
|
PLAIN_TEXT: 'Plain Text',
|
||||||
|
JSON: 'JSON',
|
||||||
|
BASE64_TO_TEXT: 'Base64 To Text',
|
||||||
|
BASE64_TO_JSON: 'Base64 To JSON',
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
{
|
||||||
|
"lang_name": "English",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"success": "Success",
|
||||||
|
"warning": "Warning",
|
||||||
|
"save": "Save",
|
||||||
|
"new_conn": "Add New Connection",
|
||||||
|
"new_group": "Add New Group",
|
||||||
|
"sort_conn": "Resort Connections",
|
||||||
|
"reload_key": "Reload Current Key",
|
||||||
|
"ttl": "TTL",
|
||||||
|
"forever": "Forever",
|
||||||
|
"rename_key": "Rename Key",
|
||||||
|
"delete_key": "Delete Key",
|
||||||
|
"delete_key_tip": "\"{key}\" will be deleted",
|
||||||
|
"delete_key_succ": "\"{key}\" has been deleted",
|
||||||
|
"copy_value": "Copy Value",
|
||||||
|
"edit_value": "Edit Value",
|
||||||
|
"save_update": "Save Update",
|
||||||
|
"cancel_update": "Cancel Update",
|
||||||
|
"add_row": "Add Row",
|
||||||
|
"edit_row": "Edit Row",
|
||||||
|
"delete_row": "Delete Row",
|
||||||
|
"search": "Search",
|
||||||
|
"view_as": "View As",
|
||||||
|
"reload": "Reload",
|
||||||
|
"open_connection": "Open Connection",
|
||||||
|
"open_db": "Expand Database",
|
||||||
|
"filter_key": "Filter Keys",
|
||||||
|
"disconnect": "Disconnect",
|
||||||
|
"dup_conn": "Duplicate Connection",
|
||||||
|
"remove_conn": "Delete Connection",
|
||||||
|
"config_conn": "Edit Connection Config",
|
||||||
|
"config_conn_group": "Edit Connection Group",
|
||||||
|
"remove_conn_group": "Delete Connection Group",
|
||||||
|
"copy_path": "Copy Path",
|
||||||
|
"remove_path": "Remove Path",
|
||||||
|
"copy_key": "Copy Key Name",
|
||||||
|
"remove_key": "Remove Key",
|
||||||
|
"new_conn_title": "New Connection",
|
||||||
|
"general": "General",
|
||||||
|
"advanced": "Advanced",
|
||||||
|
"editor": "Editor",
|
||||||
|
"conn_name": "Name",
|
||||||
|
"conn_addr": "Address",
|
||||||
|
"conn_usr": "Username",
|
||||||
|
"conn_pwd": "Password",
|
||||||
|
"conn_name_tip": "Connection name",
|
||||||
|
"conn_addr_tip": "Redis server host",
|
||||||
|
"conn_usr_tip": "(Optional) Redis server username",
|
||||||
|
"conn_pwd_tip": "(Optional) Redis server authentication password (Redis > 6.0)",
|
||||||
|
"conn_test": "Test Connection",
|
||||||
|
"conn_test_succ": "Successful connection to redis-server",
|
||||||
|
"conn_test_fail": "Fail Connection",
|
||||||
|
"conn_advn_filter": "Default Filter",
|
||||||
|
"conn_advn_filter_tip": "Pattern which defines loaded keys from redis server",
|
||||||
|
"conn_advn_separator": "Key Separator",
|
||||||
|
"conn_advn_separator_tip": "Separator used for key path item",
|
||||||
|
"conn_advn_conn_timeout": "Connection Timeout",
|
||||||
|
"conn_advn_exec_timeout": "Execution Timeout",
|
||||||
|
"conn_advn_mark_color": "Mark Color",
|
||||||
|
"new_conn_succ": "Create new connection success!",
|
||||||
|
"second": "Second(s)",
|
||||||
|
"new_key_name": "New Key Name",
|
||||||
|
"new_key": "Add New Key",
|
||||||
|
"new_field": "Add New Field",
|
||||||
|
"overwrite_field": "Overwrite Duplicated Field",
|
||||||
|
"ignore_field": "Ignore Duplicated Field",
|
||||||
|
"new_item": "Add New Item",
|
||||||
|
"insert_type": "Insert",
|
||||||
|
"append_item": "Append",
|
||||||
|
"prepend_item": "Prepend",
|
||||||
|
"enter_key": "Enter Key",
|
||||||
|
"enter_value": "Enter Value",
|
||||||
|
"enter_field": "Enter Field Name",
|
||||||
|
"enter_elem": "Enter Element",
|
||||||
|
"enter_member": "Enter Member",
|
||||||
|
"enter_score": "Enter Score",
|
||||||
|
"key": "Key",
|
||||||
|
"value": "Value",
|
||||||
|
"field": "Field",
|
||||||
|
"action": "Action",
|
||||||
|
"type": "Type",
|
||||||
|
"score": "Score",
|
||||||
|
"order_no": "Order",
|
||||||
|
"preferences": "Preferences",
|
||||||
|
"language": "Language",
|
||||||
|
"font": "Font",
|
||||||
|
"font_size": "Font Size",
|
||||||
|
"restore_defaults": "Restore Defaults",
|
||||||
|
"proxy": "Proxy",
|
||||||
|
"use_system_proxy": "Use system proxy",
|
||||||
|
"use_system_proxy_http": "Use system proxy only for HTTP(S) request",
|
||||||
|
"update": "Update",
|
||||||
|
"auto_check_update": "Automatically check for updates",
|
||||||
|
"set_ttl": "Set Key TTL",
|
||||||
|
"persist_key": "Persist Key",
|
||||||
|
"copy_value_succ": "Value Copied !",
|
||||||
|
"save_value_succ": "Value Saved !",
|
||||||
|
"handle_succ": "Handle Success !",
|
||||||
|
"field_required": "This item should not be blank",
|
||||||
|
"spec_field_required": "\"{key}\" should not be blank",
|
||||||
|
"no_connections": "No Connection",
|
||||||
|
"empty_tab_content": "Select the key from left list to see the details of the key.",
|
||||||
|
"reload_when_succ": "Reload immediately after success"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import en from './en'
|
||||||
|
import zh from './zh-cn'
|
||||||
|
|
||||||
|
export const lang = {
|
||||||
|
en,
|
||||||
|
zh,
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"lang_name": "简体中文",
|
||||||
|
"confirm": "确认",
|
||||||
|
"cancel": "取消",
|
||||||
|
"success": "成功",
|
||||||
|
"warning": "警告",
|
||||||
|
"save": "保存",
|
||||||
|
"new_conn": "添加新连接",
|
||||||
|
"new_group": "添加新分组",
|
||||||
|
"sort_conn": "调整连接顺序",
|
||||||
|
"reload_key": "重新载入此键内容",
|
||||||
|
"ttl": "TTL",
|
||||||
|
"forever": "永久",
|
||||||
|
"rename_key": "重命名键",
|
||||||
|
"delete_key": "删除键",
|
||||||
|
"delete_key_tip": "{key} 将会被删除",
|
||||||
|
"delete_key_succ": "{key} 已被删除",
|
||||||
|
"copy_value": "复制值",
|
||||||
|
"edit_value": "修改值",
|
||||||
|
"save_update": "保存修改",
|
||||||
|
"cancel_update": "取消修改",
|
||||||
|
"add_row": "插入行",
|
||||||
|
"edit_row": "编辑行",
|
||||||
|
"delete_row": "删除行",
|
||||||
|
"search": "搜索",
|
||||||
|
"filter_field": "筛选字段",
|
||||||
|
"filter_value": "筛选值",
|
||||||
|
"view_as": "查看方式",
|
||||||
|
"reload": "重新载入",
|
||||||
|
"open_connection": "打开连接",
|
||||||
|
"open_db": "展开数据库",
|
||||||
|
"filter_key": "过滤键",
|
||||||
|
"disconnect": "断开连接",
|
||||||
|
"dup_conn": "复制连接",
|
||||||
|
"remove_conn": "删除连接",
|
||||||
|
"config_conn": "编辑连接配置",
|
||||||
|
"config_conn_group": "编辑连接分组",
|
||||||
|
"remove_conn_group": "删除连接分组",
|
||||||
|
"copy_path": "复制路径",
|
||||||
|
"remove_path": "删除路径",
|
||||||
|
"copy_key": "复制键名",
|
||||||
|
"remove_key": "删除键",
|
||||||
|
"new_conn_title": "新建连接",
|
||||||
|
"general": "常规配置",
|
||||||
|
"advanced": "高级配置",
|
||||||
|
"editor": "编辑器",
|
||||||
|
"conn_name": "连接名",
|
||||||
|
"conn_addr": "连接地址",
|
||||||
|
"conn_usr": "用户名",
|
||||||
|
"conn_pwd": "密码",
|
||||||
|
"conn_name_tip": "连接名",
|
||||||
|
"conn_addr_tip": "Redis服务地址",
|
||||||
|
"conn_usr_tip": "(可选)Redis服务授权用户名",
|
||||||
|
"conn_pwd_tip": "(可选)Redis服务授权密码 (Redis > 6.0)",
|
||||||
|
"conn_test": "测试连接",
|
||||||
|
"conn_test_succ": "成功连接到Redis服务器",
|
||||||
|
"conn_test_fail": "连接失败",
|
||||||
|
"conn_advn_filter": "默认过滤",
|
||||||
|
"conn_advn_filter_tip": "需要加载的键名表达式",
|
||||||
|
"conn_advn_separator": "键分隔符",
|
||||||
|
"conn_advn_separator_tip": "键名路径分隔符",
|
||||||
|
"conn_advn_conn_timeout": "连接超时",
|
||||||
|
"conn_advn_exec_timeout": "执行超时",
|
||||||
|
"conn_advn_mark_color": "标记颜色",
|
||||||
|
"new_conn_succ": "新建连接成功",
|
||||||
|
"second": "秒",
|
||||||
|
"new_key_name": "新键名",
|
||||||
|
"new_key": "添加新键",
|
||||||
|
"new_field": "添加新字段",
|
||||||
|
"overwrite_field": "覆盖同名字段",
|
||||||
|
"ignore_field": "忽略同名字段",
|
||||||
|
"new_item": "添加新元素",
|
||||||
|
"insert_type": "插入类型",
|
||||||
|
"append_item": "尾部追加",
|
||||||
|
"prepend_item": "插入头部",
|
||||||
|
"enter_key": "输入键名",
|
||||||
|
"enter_value": "输入值",
|
||||||
|
"enter_field": "输入字段名",
|
||||||
|
"enter_elem": "输入新元素",
|
||||||
|
"enter_member": "输入成员",
|
||||||
|
"enter_score": "输入分值",
|
||||||
|
"element": "元素",
|
||||||
|
"key": "键",
|
||||||
|
"value": "值",
|
||||||
|
"field": "字段",
|
||||||
|
"action": "操作",
|
||||||
|
"type": "类型",
|
||||||
|
"score": "分值",
|
||||||
|
"order_no": "序号",
|
||||||
|
"preferences": "偏好设置",
|
||||||
|
"language": "语言",
|
||||||
|
"font": "字体",
|
||||||
|
"font_size": "字体尺寸",
|
||||||
|
"restore_defaults": "重置为默认",
|
||||||
|
"proxy": "代理",
|
||||||
|
"use_system_proxy": "使用系统代理",
|
||||||
|
"use_system_proxy_http": "仅在HTTP请求时使用系统代理",
|
||||||
|
"update": "更新",
|
||||||
|
"auto_check_update": "自动检查更新",
|
||||||
|
"set_ttl": "设置键存活时间",
|
||||||
|
"persist_key": "持久化键",
|
||||||
|
"copy_succ": "已复制到剪切板",
|
||||||
|
"save_value_succ": "已保存值",
|
||||||
|
"handle_succ": "操作成功",
|
||||||
|
"field_required": "此项不能为空",
|
||||||
|
"spec_field_required": "{key} 不能为空",
|
||||||
|
"no_connections": "空空如也",
|
||||||
|
"empty_tab_content": "可以从左边选择键来查看键的详细内容",
|
||||||
|
"reload_when_succ": "操作成功后立即重新加载"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import App from './App.vue'
|
||||||
|
import { lang } from './langs'
|
||||||
|
import './style.scss'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(
|
||||||
|
createI18n({
|
||||||
|
locale: 'en',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
globalInjection: true,
|
||||||
|
legacy: false,
|
||||||
|
messages: {
|
||||||
|
...lang,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.mount('#app')
|
|
@ -0,0 +1,904 @@
|
||||||
|
import { get, isEmpty, last, remove, size, sortedIndexBy, split } from 'lodash'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import {
|
||||||
|
AddHashField,
|
||||||
|
AddListItem,
|
||||||
|
AddZSetValue,
|
||||||
|
CloseConnection,
|
||||||
|
GetKeyValue,
|
||||||
|
ListConnection,
|
||||||
|
OpenConnection,
|
||||||
|
OpenDatabase,
|
||||||
|
RemoveKey,
|
||||||
|
RenameKey,
|
||||||
|
SetHashValue,
|
||||||
|
SetKeyTTL,
|
||||||
|
SetKeyValue,
|
||||||
|
SetListItem,
|
||||||
|
SetSetItem,
|
||||||
|
UpdateSetItem,
|
||||||
|
UpdateZSetValue,
|
||||||
|
} from '../../wailsjs/go/services/connectionService.js'
|
||||||
|
import { ConnectionType } from '../consts/connection_type.js'
|
||||||
|
import useTabStore from './tab.js'
|
||||||
|
|
||||||
|
const separator = ':'
|
||||||
|
|
||||||
|
const useConnectionStore = defineStore('connection', {
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ConnectionItem
|
||||||
|
* @property {string} key
|
||||||
|
* @property {string} label
|
||||||
|
* @property {string} name - server name, type != ConnectionType.Group only
|
||||||
|
* @property {number} type
|
||||||
|
* @property {number} [db] - database index, type == ConnectionType.RedisDB only
|
||||||
|
* @property {number} keys
|
||||||
|
* @property {boolean} [connected] - redis server is connected, type == ConnectionType.Server only
|
||||||
|
* @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
|
||||||
|
* @property {boolean} [expanded] - current node is expanded
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {{connections: ConnectionItem[]}}
|
||||||
|
*/
|
||||||
|
state: () => ({
|
||||||
|
connections: [], // all connections list
|
||||||
|
}),
|
||||||
|
getters: {},
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* Load all store connections struct from local profile
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initConnection() {
|
||||||
|
if (!isEmpty(this.connections)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { data = [{ groupName: '', connections: [] }] } = await ListConnection()
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const group = data[i]
|
||||||
|
// Top level group
|
||||||
|
if (isEmpty(group.groupName)) {
|
||||||
|
for (let j = 0; j < group.connections.length; j++) {
|
||||||
|
const item = group.connections[j]
|
||||||
|
this.connections.push({
|
||||||
|
key: item.name,
|
||||||
|
label: item.name,
|
||||||
|
name: item.name,
|
||||||
|
type: ConnectionType.Server,
|
||||||
|
// isLeaf: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Custom group
|
||||||
|
const children = []
|
||||||
|
for (let j = 0; j < group.connections.length; j++) {
|
||||||
|
const item = group.connections[j]
|
||||||
|
const value = group.groupName + '/' + item.name
|
||||||
|
children.push({
|
||||||
|
key: value,
|
||||||
|
label: item.name,
|
||||||
|
name: item.name,
|
||||||
|
type: ConnectionType.Server,
|
||||||
|
children: j === group.connections.length - 1 ? undefined : [],
|
||||||
|
// isLeaf: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.connections.push({
|
||||||
|
key: group.groupName,
|
||||||
|
label: group.groupName,
|
||||||
|
type: ConnectionType.Group,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(JSON.stringify(this.connections))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open connection
|
||||||
|
* @param {string} connName
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async openConnection(connName) {
|
||||||
|
const { data, success, msg } = await OpenConnection(connName)
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
// append to db node to current connection
|
||||||
|
const connNode = this.getConnection(connName)
|
||||||
|
if (connNode == null) {
|
||||||
|
throw new Error('no such connection')
|
||||||
|
}
|
||||||
|
const { db } = data
|
||||||
|
if (isEmpty(db)) {
|
||||||
|
throw new Error('no db loaded')
|
||||||
|
}
|
||||||
|
const children = []
|
||||||
|
for (let i = 0; i < db.length; i++) {
|
||||||
|
children.push({
|
||||||
|
key: `${connName}/${db[i].name}`,
|
||||||
|
label: db[i].name,
|
||||||
|
name: connName,
|
||||||
|
keys: db[i].keys,
|
||||||
|
db: i,
|
||||||
|
type: ConnectionType.RedisDB,
|
||||||
|
// isLeaf: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
connNode.children = children
|
||||||
|
connNode.connected = true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close connection
|
||||||
|
* @param {string} connName
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async closeConnection(connName) {
|
||||||
|
const { success, msg } = await CloseConnection(connName)
|
||||||
|
if (!success) {
|
||||||
|
// throw new Error(msg)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// successful close connection, remove all children
|
||||||
|
const connNode = this.getConnection(connName)
|
||||||
|
if (connNode == null) {
|
||||||
|
// throw new Error('no such connection')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
connNode.children = undefined
|
||||||
|
connNode.isLeaf = undefined
|
||||||
|
connNode.connected = false
|
||||||
|
connNode.expanded = false
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Connection by path name
|
||||||
|
* @param {string} connName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getConnection(connName) {
|
||||||
|
const conn = this.connections
|
||||||
|
for (let i = 0; i < conn.length; i++) {
|
||||||
|
if (conn[i].type === ConnectionType.Server && conn[i].key === connName) {
|
||||||
|
return conn[i]
|
||||||
|
} else if (conn[i].type === ConnectionType.Group) {
|
||||||
|
const children = conn[i].children
|
||||||
|
for (let j = 0; j < children.length; j++) {
|
||||||
|
if (children[j].type === ConnectionType.Server && conn[i].key === connName) {
|
||||||
|
return children[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open database and load all keys
|
||||||
|
* @param connName
|
||||||
|
* @param db
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async openDatabase(connName, db) {
|
||||||
|
const { data, success, msg } = await OpenDatabase(connName, db)
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
const { keys = [] } = data
|
||||||
|
if (isEmpty(keys)) {
|
||||||
|
const connNode = this.getConnection(connName)
|
||||||
|
const { children = [] } = connNode
|
||||||
|
children[db].children = []
|
||||||
|
children[db].opened = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert child to children list by order
|
||||||
|
const sortedInsertChild = (childrenList, item) => {
|
||||||
|
const insertIdx = sortedIndexBy(childrenList, item, 'key')
|
||||||
|
childrenList.splice(insertIdx, 0, item)
|
||||||
|
// childrenList.push(item)
|
||||||
|
}
|
||||||
|
// update all node item's children num
|
||||||
|
const updateChildrenNum = (node) => {
|
||||||
|
let count = 0
|
||||||
|
const totalChildren = size(node.children)
|
||||||
|
if (totalChildren > 0) {
|
||||||
|
for (const elem of node.children) {
|
||||||
|
updateChildrenNum(elem)
|
||||||
|
count += elem.keys
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
node.keys = count
|
||||||
|
// node.children = sortBy(node.children, 'label')
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyStruct = []
|
||||||
|
const mark = {}
|
||||||
|
for (const key in keys) {
|
||||||
|
const keyPart = split(key, separator)
|
||||||
|
// const prefixLen = size(keyPart) - 1
|
||||||
|
const len = size(keyPart)
|
||||||
|
let handlePath = ''
|
||||||
|
let ks = keyStruct
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
handlePath += keyPart[i]
|
||||||
|
if (i !== len - 1) {
|
||||||
|
// layer
|
||||||
|
const treeKey = `${handlePath}@${ConnectionType.RedisKey}`
|
||||||
|
if (!mark.hasOwnProperty(treeKey)) {
|
||||||
|
mark[treeKey] = {
|
||||||
|
key: `${connName}/db${db}/${treeKey}`,
|
||||||
|
label: keyPart[i],
|
||||||
|
name: connName,
|
||||||
|
db,
|
||||||
|
keys: 0,
|
||||||
|
redisKey: handlePath,
|
||||||
|
type: ConnectionType.RedisKey,
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
sortedInsertChild(ks, mark[treeKey])
|
||||||
|
}
|
||||||
|
ks = mark[treeKey].children
|
||||||
|
handlePath += separator
|
||||||
|
} else {
|
||||||
|
// key
|
||||||
|
const treeKey = `${handlePath}@${ConnectionType.RedisValue}`
|
||||||
|
mark[treeKey] = {
|
||||||
|
key: `${connName}/db${db}/${treeKey}`,
|
||||||
|
label: keyPart[i],
|
||||||
|
name: connName,
|
||||||
|
db,
|
||||||
|
keys: 0,
|
||||||
|
redisKey: handlePath,
|
||||||
|
type: ConnectionType.RedisValue,
|
||||||
|
}
|
||||||
|
sortedInsertChild(ks, mark[treeKey])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append db node to current connection's children
|
||||||
|
const connNode = this.getConnection(connName)
|
||||||
|
const { children = [] } = connNode
|
||||||
|
children[db].children = keyStruct
|
||||||
|
children[db].opened = true
|
||||||
|
updateChildrenNum(children[db])
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* select node
|
||||||
|
* @param key
|
||||||
|
* @param name
|
||||||
|
* @param db
|
||||||
|
* @param type
|
||||||
|
* @param redisKey
|
||||||
|
*/
|
||||||
|
select({ key, name, db, type, redisKey }) {
|
||||||
|
if (type === ConnectionType.RedisValue) {
|
||||||
|
console.log(`[click]key:${key} db: ${db} redis key: ${redisKey}`)
|
||||||
|
|
||||||
|
// async get value for key
|
||||||
|
this.loadKeyValue(name, db, redisKey).then(() => {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load redis key
|
||||||
|
* @param server
|
||||||
|
* @param db
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
async loadKeyValue(server, db, key) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await GetKeyValue(server, db, key)
|
||||||
|
if (success) {
|
||||||
|
const { type, ttl, value } = data
|
||||||
|
const tab = useTabStore()
|
||||||
|
tab.upsertTab({
|
||||||
|
server,
|
||||||
|
db,
|
||||||
|
type,
|
||||||
|
ttl,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.warn('TODO: handle get key fail')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_addKey(connName, db, key) {
|
||||||
|
const connNode = this.getConnection(connName)
|
||||||
|
const { children: dbs = [] } = connNode
|
||||||
|
const dbDetail = get(dbs, db, {})
|
||||||
|
|
||||||
|
if (dbDetail == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const descendantChain = [dbDetail]
|
||||||
|
|
||||||
|
const keyPart = split(key, separator)
|
||||||
|
let redisKey = ''
|
||||||
|
const keyLen = size(keyPart)
|
||||||
|
let added = false
|
||||||
|
for (let i = 0; i < keyLen; i++) {
|
||||||
|
redisKey += keyPart[i]
|
||||||
|
|
||||||
|
const node = last(descendantChain)
|
||||||
|
const nodeList = get(node, 'children', [])
|
||||||
|
const len = size(nodeList)
|
||||||
|
const isLastKeyPart = i === keyLen - 1
|
||||||
|
for (let j = 0; j < len + 1; j++) {
|
||||||
|
const treeKey = get(nodeList[j], 'key')
|
||||||
|
const isLast = j >= len - 1
|
||||||
|
const currentKey = `${connName}/db${db}/${redisKey}@${
|
||||||
|
isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey
|
||||||
|
}`
|
||||||
|
if (treeKey > currentKey || isLast) {
|
||||||
|
// out of search range, add new item
|
||||||
|
if (isLastKeyPart) {
|
||||||
|
// key not exists, add new one
|
||||||
|
const item = {
|
||||||
|
key: currentKey,
|
||||||
|
label: keyPart[i],
|
||||||
|
name: connName,
|
||||||
|
db,
|
||||||
|
keys: 1,
|
||||||
|
redisKey,
|
||||||
|
type: ConnectionType.RedisValue,
|
||||||
|
}
|
||||||
|
if (isLast) {
|
||||||
|
nodeList.push(item)
|
||||||
|
} else {
|
||||||
|
nodeList.splice(j, 0, item)
|
||||||
|
}
|
||||||
|
added = true
|
||||||
|
} else {
|
||||||
|
// layer not exists, add new one
|
||||||
|
const item = {
|
||||||
|
key: currentKey,
|
||||||
|
label: keyPart[i],
|
||||||
|
name: connName,
|
||||||
|
db,
|
||||||
|
keys: 0,
|
||||||
|
redisKey,
|
||||||
|
type: ConnectionType.RedisKey,
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
if (isLast) {
|
||||||
|
nodeList.push(item)
|
||||||
|
descendantChain.push(last(nodeList))
|
||||||
|
} else {
|
||||||
|
nodeList.splice(j, 0, item)
|
||||||
|
descendantChain.push(nodeList[j])
|
||||||
|
}
|
||||||
|
redisKey += separator
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else if (treeKey === currentKey) {
|
||||||
|
if (isLastKeyPart) {
|
||||||
|
// same key exists, do nothing
|
||||||
|
console.log('TODO: same key exist, do nothing now, should replace value later')
|
||||||
|
} else {
|
||||||
|
// same group exists, find into it's children
|
||||||
|
descendantChain.push(nodeList[j])
|
||||||
|
redisKey += separator
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update ancestor node's info
|
||||||
|
if (added) {
|
||||||
|
const desLen = size(descendantChain)
|
||||||
|
for (let i = 0; i < desLen; i++) {
|
||||||
|
const children = get(descendantChain[i], 'children', [])
|
||||||
|
let keys = 0
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.type === ConnectionType.RedisKey) {
|
||||||
|
keys += get(child, 'keys', 1)
|
||||||
|
} else if (child.type === ConnectionType.RedisValue) {
|
||||||
|
keys += get(child, 'keys', 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descendantChain[i].keys = keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set redis key
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number} keyType
|
||||||
|
* @param {any} value
|
||||||
|
* @param {number} ttl
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async setKey(connName, db, key, keyType, value, ttl) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl)
|
||||||
|
if (success) {
|
||||||
|
// update tree view data
|
||||||
|
this._addKey(connName, db, key)
|
||||||
|
return { success }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update hash field
|
||||||
|
* when field is set, newField is null, delete field
|
||||||
|
* when field is null, newField is set, add new field
|
||||||
|
* when both field and newField are set, and field === newField, update field
|
||||||
|
* when both field and newField are set, and field !== newField, delete field and add newField
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} field
|
||||||
|
* @param {string} newField
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
|
||||||
|
*/
|
||||||
|
async setHash(connName, db, key, field, newField, value) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '')
|
||||||
|
if (success) {
|
||||||
|
const { updated = {} } = data
|
||||||
|
return { success, updated }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert or update hash field item
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields
|
||||||
|
* @param {string[]} fieldItems field1, value1, filed2, value2...
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
|
||||||
|
*/
|
||||||
|
async addHashField(connName, db, key, action, fieldItems) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems)
|
||||||
|
if (success) {
|
||||||
|
const { updated = {} } = data
|
||||||
|
return { success, updated }
|
||||||
|
} else {
|
||||||
|
return { success: false, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove hash field
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} field
|
||||||
|
* @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>}
|
||||||
|
*/
|
||||||
|
async removeHashField(connName, db, key, field) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '')
|
||||||
|
if (success) {
|
||||||
|
const { removed = [] } = data
|
||||||
|
return { success, removed }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert list item
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {int} action 0: push to head, 1: push to tail
|
||||||
|
* @param {string[]}values
|
||||||
|
* @returns {Promise<*|{msg, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async addListItem(connName, db, key, action, values) {
|
||||||
|
try {
|
||||||
|
return AddListItem(connName, db, key, action, values)
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prepend item to head of list
|
||||||
|
* @param connName
|
||||||
|
* @param db
|
||||||
|
* @param key
|
||||||
|
* @param values
|
||||||
|
* @returns {Promise<[msg]: string, success: boolean, [item]: []>}
|
||||||
|
*/
|
||||||
|
async prependListItem(connName, db, key, values) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await AddListItem(connName, db, key, 0, values)
|
||||||
|
if (success) {
|
||||||
|
const { left = [] } = data
|
||||||
|
return { success, item: left }
|
||||||
|
} else {
|
||||||
|
return { success: false, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* append item to tail of list
|
||||||
|
* @param connName
|
||||||
|
* @param db
|
||||||
|
* @param key
|
||||||
|
* @param values
|
||||||
|
* @returns {Promise<[msg]: string, success: boolean, [item]: any[]>}
|
||||||
|
*/
|
||||||
|
async appendListItem(connName, db, key, values) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await AddListItem(connName, db, key, 1, values)
|
||||||
|
if (success) {
|
||||||
|
const { right = [] } = data
|
||||||
|
return { success, item: right }
|
||||||
|
} else {
|
||||||
|
return { success: false, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update value of list item by index
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number} index
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>}
|
||||||
|
*/
|
||||||
|
async updateListItem(connName, db, key, index, value) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await SetListItem(connName, db, key, index, value)
|
||||||
|
if (success) {
|
||||||
|
const { updated = {} } = data
|
||||||
|
return { success, updated }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove list item
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number} index
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>}
|
||||||
|
*/
|
||||||
|
async removeListItem(connName, db, key, index) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await SetListItem(connName, db, key, index, '')
|
||||||
|
if (success) {
|
||||||
|
const { removed = [] } = data
|
||||||
|
return { success, removed }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add item to set
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async addSetItem(connName, db, key, value) {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await SetSetItem(connName, db, key, false, [value])
|
||||||
|
if (success) {
|
||||||
|
return { success }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update value of set item
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} value
|
||||||
|
* @param {string} newValue
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async updateSetItem(connName, db, key, value, newValue) {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue)
|
||||||
|
if (success) {
|
||||||
|
return { success: true }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove item from set
|
||||||
|
* @param connName
|
||||||
|
* @param db
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async removeSetItem(connName, db, key, value) {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await SetSetItem(connName, db, key, true, [value])
|
||||||
|
if (success) {
|
||||||
|
return { success }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add item to sorted set
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number} action
|
||||||
|
* @param {Object.<string, number>} vs value: score
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async addZSetItem(connName, db, key, action, vs) {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await AddZSetValue(connName, db, key, action, vs)
|
||||||
|
if (success) {
|
||||||
|
return { success }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update item of sorted set
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} value
|
||||||
|
* @param {string} newValue
|
||||||
|
* @param {number} score
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>}
|
||||||
|
*/
|
||||||
|
async updateZSetItem(connName, db, key, value, newValue, score) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score)
|
||||||
|
if (success) {
|
||||||
|
const { updated, removed } = data
|
||||||
|
return { success, updated, removed }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove item from sorted set
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param key
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>}
|
||||||
|
*/
|
||||||
|
async removeZSetItem(connName, db, key, value) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0)
|
||||||
|
if (success) {
|
||||||
|
const { removed } = data
|
||||||
|
return { success, removed }
|
||||||
|
} else {
|
||||||
|
return { success, msg }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, msg: e.message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reset key's ttl
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {number} ttl
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async setTTL(connName, db, key, ttl) {
|
||||||
|
try {
|
||||||
|
const { success, msg } = await SetKeyTTL(connName, db, key, ttl)
|
||||||
|
return success === true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_removeKey(connName, db, key) {
|
||||||
|
const connNode = this.getConnection(connName)
|
||||||
|
const { children: dbs = [] } = connNode
|
||||||
|
const dbDetail = get(dbs, db, {})
|
||||||
|
|
||||||
|
if (dbDetail == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const descendantChain = [dbDetail]
|
||||||
|
const keyPart = split(key, separator)
|
||||||
|
let redisKey = ''
|
||||||
|
const keyLen = size(keyPart)
|
||||||
|
let deleted = false
|
||||||
|
let forceBreak = false
|
||||||
|
for (let i = 0; i < keyLen && !forceBreak; i++) {
|
||||||
|
redisKey += keyPart[i]
|
||||||
|
|
||||||
|
const node = last(descendantChain)
|
||||||
|
const nodeList = get(node, 'children', [])
|
||||||
|
const len = size(nodeList)
|
||||||
|
const isLastKeyPart = i === keyLen - 1
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
const treeKey = get(nodeList[j], 'key')
|
||||||
|
const currentKey = `${connName}/db${db}/${redisKey}@${
|
||||||
|
isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey
|
||||||
|
}`
|
||||||
|
if (treeKey > currentKey) {
|
||||||
|
// out of search range, target not exists
|
||||||
|
forceBreak = true
|
||||||
|
break
|
||||||
|
} else if (treeKey === currentKey) {
|
||||||
|
if (isLastKeyPart) {
|
||||||
|
// find target
|
||||||
|
nodeList.splice(j, 1)
|
||||||
|
node.keys -= 1
|
||||||
|
deleted = true
|
||||||
|
forceBreak = true
|
||||||
|
} else {
|
||||||
|
// find into it's children
|
||||||
|
descendantChain.push(nodeList[j])
|
||||||
|
redisKey += separator
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceBreak) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(JSON.stringify(descendantChain))
|
||||||
|
|
||||||
|
// update ancestor node's info
|
||||||
|
if (deleted) {
|
||||||
|
const desLen = size(descendantChain)
|
||||||
|
for (let i = desLen - 1; i > 0; i--) {
|
||||||
|
const children = get(descendantChain[i], 'children', [])
|
||||||
|
const parent = descendantChain[i - 1]
|
||||||
|
if (isEmpty(children)) {
|
||||||
|
const parentChildren = get(parent, 'children', [])
|
||||||
|
const k = get(descendantChain[i], 'key')
|
||||||
|
remove(parentChildren, (item) => item.key === k)
|
||||||
|
}
|
||||||
|
parent.keys -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove redis key
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async removeKey(connName, db, key) {
|
||||||
|
try {
|
||||||
|
const { data, success, msg } = await RemoveKey(connName, db, key)
|
||||||
|
if (success) {
|
||||||
|
// update tree view data
|
||||||
|
this._removeKey(connName, db, key)
|
||||||
|
|
||||||
|
// set tab content empty
|
||||||
|
const tab = useTabStore()
|
||||||
|
tab.emptyTab(connName)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rename key
|
||||||
|
* @param {string} connName
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} newKey
|
||||||
|
* @returns {Promise<{[msg]: string, success: boolean}>}
|
||||||
|
*/
|
||||||
|
async renameKey(connName, db, key, newKey) {
|
||||||
|
const { success = false, msg } = await RenameKey(connName, db, key, newKey)
|
||||||
|
if (success) {
|
||||||
|
// delete old key and add new key struct
|
||||||
|
this._removeKey(connName, db, key)
|
||||||
|
this._addKey(connName, db, newKey)
|
||||||
|
return { success: true }
|
||||||
|
} else {
|
||||||
|
return { success: false, msg }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useConnectionStore
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
const useDialogStore = defineStore('dialog', {
|
||||||
|
state: () => ({
|
||||||
|
newDialogVisible: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property {string} prefix
|
||||||
|
* @property {string} server
|
||||||
|
* @property {int} db
|
||||||
|
*/
|
||||||
|
newKeyParam: {
|
||||||
|
prefix: '',
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
},
|
||||||
|
newKeyDialogVisible: false,
|
||||||
|
|
||||||
|
addFieldParam: {
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
key: '',
|
||||||
|
type: null,
|
||||||
|
},
|
||||||
|
addFieldsDialogVisible: false,
|
||||||
|
|
||||||
|
renameKeyParam: {
|
||||||
|
server: '',
|
||||||
|
db: 0,
|
||||||
|
key: '',
|
||||||
|
},
|
||||||
|
renameDialogVisible: false,
|
||||||
|
|
||||||
|
selectTTL: -1,
|
||||||
|
ttlDialogVisible: false,
|
||||||
|
|
||||||
|
preferencesDialogVisible: false,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
openNewDialog() {
|
||||||
|
this.newDialogVisible = true
|
||||||
|
},
|
||||||
|
closeNewDialog() {
|
||||||
|
this.newDialogVisible = false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} server
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
openRenameKeyDialog(server, db, key) {
|
||||||
|
this.renameKeyParam.server = server
|
||||||
|
this.renameKeyParam.db = db
|
||||||
|
this.renameKeyParam.key = key
|
||||||
|
this.renameDialogVisible = true
|
||||||
|
},
|
||||||
|
closeRenameKeyDialog() {
|
||||||
|
this.renameDialogVisible = false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} prefix
|
||||||
|
* @param {number} server
|
||||||
|
* @param {string} db
|
||||||
|
*/
|
||||||
|
openNewKeyDialog(prefix, server, db) {
|
||||||
|
this.newKeyParam.prefix = prefix
|
||||||
|
this.newKeyParam.server = server
|
||||||
|
this.newKeyParam.db = db
|
||||||
|
this.newKeyDialogVisible = true
|
||||||
|
},
|
||||||
|
closeNewKeyDialog() {
|
||||||
|
this.newKeyDialogVisible = false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} server
|
||||||
|
* @param {number} db
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} type
|
||||||
|
*/
|
||||||
|
openAddFieldsDialog(server, db, key, type) {
|
||||||
|
this.addFieldParam.server = server
|
||||||
|
this.addFieldParam.db = db
|
||||||
|
this.addFieldParam.key = key
|
||||||
|
this.addFieldParam.type = type
|
||||||
|
this.addFieldsDialogVisible = true
|
||||||
|
},
|
||||||
|
closeAddFieldsDialog() {
|
||||||
|
this.addFieldsDialogVisible = false
|
||||||
|
},
|
||||||
|
|
||||||
|
openTTLDialog(ttl) {
|
||||||
|
this.selectTTL = ttl
|
||||||
|
this.ttlDialogVisible = true
|
||||||
|
},
|
||||||
|
closeTTLDialog() {
|
||||||
|
this.selectTTL = -1
|
||||||
|
this.ttlDialogVisible = false
|
||||||
|
},
|
||||||
|
|
||||||
|
openPreferencesDialog() {
|
||||||
|
this.preferencesDialogVisible = true
|
||||||
|
},
|
||||||
|
closePreferencesDialog() {
|
||||||
|
this.preferencesDialogVisible = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useDialogStore
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue