Просмотр исходного кода

refactor: refactor app and api

0xJacky 1 год назад
Родитель
Сommit
287ef7527d
100 измененных файлов с 4496 добавлено и 1678 удалено
  1. 6 3
      .air.toml
  2. 2 2
      api/certificate/router.go
  3. 56 0
      api/config/add.go
  4. 0 206
      api/config/config.go
  5. 51 0
      api/config/get.go
  6. 69 0
      api/config/list.go
  7. 51 0
      api/config/modify.go
  8. 134 0
      api/cosy/cosy.go
  9. 72 0
      api/cosy/create.go
  10. 39 0
      api/cosy/custom.go
  11. 91 0
      api/cosy/delete.go
  12. 23 0
      api/cosy/error.go
  13. 158 0
      api/cosy/list.go
  14. 56 0
      api/cosy/map2struct/hook.go
  15. 24 0
      api/cosy/map2struct/map2struct.go
  16. 42 0
      api/cosy/order.go
  17. 90 0
      api/cosy/update.go
  18. 5 6
      api/nginx/nginx.go
  19. 5 40
      api/openai/openai.go
  20. 44 0
      api/openai/store.go
  21. 42 0
      api/sites/advance.go
  22. 64 0
      api/sites/auto_cert.go
  23. 70 206
      api/sites/domain.go
  24. 44 0
      api/sites/duplicate.go
  25. 26 0
      api/sites/sites.go
  26. 1 1
      api/template/template.go
  27. 258 0
      app/.eslintrc.js
  28. 16 8
      app/components.d.ts
  29. 1 0
      app/gettext.config.js
  30. 12 0
      app/package.json
  31. 1221 5
      app/pnpm-lock.yaml
  32. 10 9
      app/src/App.vue
  33. 1 1
      app/src/api/analytic.ts
  34. 14 11
      app/src/api/auth.ts
  35. 22 5
      app/src/api/auto_cert.ts
  36. 23 1
      app/src/api/cert.ts
  37. 10 1
      app/src/api/config.ts
  38. 31 9
      app/src/api/curd.ts
  39. 9 1
      app/src/api/dns_credential.ts
  40. 35 12
      app/src/api/domain.ts
  41. 9 2
      app/src/api/install.ts
  42. 3 2
      app/src/api/nginx_log.ts
  43. 37 4
      app/src/api/ngx.ts
  44. 8 2
      app/src/api/openai.ts
  45. 2 1
      app/src/api/settings.ts
  46. 25 7
      app/src/api/template.ts
  47. 3 3
      app/src/api/upgrade.ts
  48. 17 15
      app/src/components/Breadcrumb/Breadcrumb.vue
  49. 57 46
      app/src/components/Chart/AreaChart.vue
  50. 45 27
      app/src/components/Chart/RadialBarChart.vue
  51. 15 12
      app/src/components/Chart/UsageProgressLine.vue
  52. 4 0
      app/src/components/Chart/types.d.ts
  53. 154 97
      app/src/components/ChatGPT/ChatGPT.vue
  54. 14 10
      app/src/components/CodeEditor/CodeEditor.vue
  55. 1 1
      app/src/components/CodeEditor/index.ts
  56. 25 19
      app/src/components/EnvIndicator/EnvIndicator.vue
  57. 7 5
      app/src/components/FooterToolbar/FooterToolBar.vue
  58. 7 2
      app/src/components/Logo/Logo.vue
  59. 58 36
      app/src/components/NginxControl/NginxControl.vue
  60. 53 21
      app/src/components/NodeSelector/NodeSelector.vue
  61. 11 11
      app/src/components/PageHeader/PageHeader.vue
  62. 20 12
      app/src/components/SetLanguage/SetLanguage.vue
  63. 0 37
      app/src/components/StdDataEntry/StdDataEntry.tsx
  64. 0 45
      app/src/components/StdDataEntry/components/StdSelect.vue
  65. 0 133
      app/src/components/StdDataEntry/index.tsx
  66. 18 17
      app/src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue
  67. 56 39
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  68. 16 10
      app/src/components/StdDesign/StdDataDisplay/StdPagination.vue
  69. 183 152
      app/src/components/StdDesign/StdDataDisplay/StdTable.vue
  70. 8 6
      app/src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx
  71. 0 0
      app/src/components/StdDesign/StdDataDisplay/index.ts
  72. 54 0
      app/src/components/StdDesign/StdDataEntry/StdDataEntry.tsx
  73. 20 14
      app/src/components/StdDesign/StdDataEntry/StdFormItem.vue
  74. 32 14
      app/src/components/StdDesign/StdDataEntry/components/StdPassword.vue
  75. 53 0
      app/src/components/StdDesign/StdDataEntry/components/StdSelect.vue
  76. 53 39
      app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue
  77. 101 0
      app/src/components/StdDesign/StdDataEntry/index.tsx
  78. 0 0
      app/src/components/StdDesign/StdDataEntry/style.less
  79. 49 0
      app/src/components/StdDesign/types.d.ts
  80. 12 13
      app/src/components/SwitchAppearance/SwitchAppearance.vue
  81. 7 3
      app/src/components/SwitchAppearance/icons/VPIconMoon.vue
  82. 15 15
      app/src/components/SwitchAppearance/icons/VPIconSun.vue
  83. 10 3
      app/src/components/VPSwitch/VPSwitch.vue
  84. 2 2
      app/src/gettext.ts
  85. 2 2
      app/src/language/constants.ts
  86. 49 44
      app/src/layouts/BaseLayout.vue
  87. 5 5
      app/src/layouts/BaseRouterView.vue
  88. 4 11
      app/src/layouts/FooterLayout.vue
  89. 19 13
      app/src/layouts/HeaderLayout.vue
  90. 21 20
      app/src/layouts/Loading.vue
  91. 54 44
      app/src/layouts/SideBar.vue
  92. 17 31
      app/src/lib/helper/index.ts
  93. 26 15
      app/src/lib/http/index.ts
  94. 7 9
      app/src/lib/websocket/index.ts
  95. 7 5
      app/src/main.ts
  96. 3 3
      app/src/pinia/index.ts
  97. 7 7
      app/src/pinia/moudule/settings.ts
  98. 5 5
      app/src/pinia/moudule/user.ts
  99. 77 59
      app/src/routes/index.ts
  100. 1 1
      app/src/version.json

+ 6 - 3
.air.toml

@@ -13,11 +13,11 @@ bin = "tmp/main"
 # Customize binary.
 full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
 # Watch these filename extensions.
-include_ext = ["go", "tpl", "tmpl", "html", "toml"]
+include_ext = ["go", "tpl", "tmpl", "html", "toml", "po"]
 # Ignore these filename extensions or directories.
-exclude_dir = ["assets", "tmp", "vendor", "app/node_modules", "upload", "docs", "resources", .ts", ".vue", ".tsx", ".idea"]
+exclude_dir = ["assets", "tmp", "vendor", "app/node_modules", "upload", "docs", "resources", ".idea"]
 # Watch these directories if you specified.
-include_dir = ["app/src/language"]
+include_dir = []
 # Exclude files.
 exclude_file = []
 # Exclude specific regular expressions.
@@ -51,3 +51,6 @@ runner = "green"
 [misc]
 # Delete tmp directory on exit
 clean_on_exit = true
+
+[screen]
+keep_scroll = true

+ 2 - 2
api/certificate/router.go

@@ -17,6 +17,6 @@ func InitCertificateRouter(r *gin.RouterGroup) {
 	r.POST("cert", AddCert)
 	r.POST("cert/:id", ModifyCert)
 	r.DELETE("cert/:id", RemoveCert)
-	r.GET("auto_cert/dns/providers", GetDNSProvidersList)
-	r.GET("auto_cert/dns/provider/:code", GetDNSProvider)
+	r.GET("certificate/dns_providers", GetDNSProvidersList)
+	r.GET("certificate/dns_provider/:code", GetDNSProvider)
 }

+ 56 - 0
api/config/add.go

@@ -0,0 +1,56 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"os"
+)
+
+func AddConfig(c *gin.Context) {
+	var request struct {
+		Name    string `json:"name" binding:"required"`
+		Content string `json:"content" binding:"required"`
+	}
+
+	err := c.BindJSON(&request)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	name := request.Name
+	content := request.Content
+
+	path := nginx.GetConfPath("/", name)
+
+	if _, err = os.Stat(path); err == nil {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "config exist",
+		})
+		return
+	}
+
+	if content != "" {
+		err = os.WriteFile(path, []byte(content), 0644)
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+	}
+
+	output := nginx.Reload()
+	if nginx.GetLogLevel(output) >= nginx.Warn {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, config.Config{
+		Name:    name,
+		Content: content,
+	})
+}

+ 0 - 206
api/config/config.go

@@ -1,206 +0,0 @@
-package config
-
-import (
-	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/internal/config_list"
-	"github.com/0xJacky/Nginx-UI/internal/logger"
-	nginx2 "github.com/0xJacky/Nginx-UI/internal/nginx"
-	"github.com/0xJacky/Nginx-UI/query"
-	"github.com/gin-gonic/gin"
-	"github.com/sashabaranov/go-openai"
-	"net/http"
-	"os"
-)
-
-func GetConfigs(c *gin.Context) {
-	orderBy := c.Query("order_by")
-	sort := c.DefaultQuery("sort", "desc")
-	dir := c.DefaultQuery("dir", "/")
-
-	mySort := map[string]string{
-		"name":   "string",
-		"modify": "time",
-		"is_dir": "bool",
-	}
-
-	configFiles, err := os.ReadDir(nginx2.GetConfPath(dir))
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	var configs []gin.H
-
-	for i := range configFiles {
-		file := configFiles[i]
-		fileInfo, _ := file.Info()
-
-		switch mode := fileInfo.Mode(); {
-		case mode.IsRegular(): // regular file, not a hidden file
-			if "." == file.Name()[0:1] {
-				continue
-			}
-		case mode&os.ModeSymlink != 0: // is a symbol
-			var targetPath string
-			targetPath, err = os.Readlink(nginx2.GetConfPath(file.Name()))
-			if err != nil {
-				logger.Error("Read Symlink Error", targetPath, err)
-				continue
-			}
-
-			var targetInfo os.FileInfo
-			targetInfo, err = os.Stat(targetPath)
-			if err != nil {
-				logger.Error("Stat Error", targetPath, err)
-				continue
-			}
-			// but target file is not a dir
-			if targetInfo.IsDir() {
-				continue
-			}
-		}
-
-		configs = append(configs, gin.H{
-			"name":   file.Name(),
-			"size":   fileInfo.Size(),
-			"modify": fileInfo.ModTime(),
-			"is_dir": file.IsDir(),
-		})
-	}
-
-	configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
-
-	c.JSON(http.StatusOK, gin.H{
-		"data": configs,
-	})
-}
-
-func GetConfig(c *gin.Context) {
-	name := c.Param("name")
-	path := nginx2.GetConfPath("/", name)
-
-	stat, err := os.Stat(path)
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	content, err := os.ReadFile(path)
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	g := query.ChatGPTLog
-	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	if chatgpt.Content == nil {
-		chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"config":           string(content),
-		"chatgpt_messages": chatgpt.Content,
-		"file_path":        path,
-		"modified_at":      stat.ModTime(),
-	})
-
-}
-
-type AddConfigJson struct {
-	Name    string `json:"name" binding:"required"`
-	Content string `json:"content" binding:"required"`
-}
-
-func AddConfig(c *gin.Context) {
-	var request AddConfigJson
-	err := c.BindJSON(&request)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	name := request.Name
-	content := request.Content
-
-	path := nginx2.GetConfPath("/", name)
-
-	if _, err = os.Stat(path); err == nil {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "config exist",
-		})
-		return
-	}
-
-	if content != "" {
-		err = os.WriteFile(path, []byte(content), 0644)
-		if err != nil {
-			api.ErrHandler(c, err)
-			return
-		}
-	}
-
-	output := nginx2.Reload()
-	if nginx2.GetLogLevel(output) >= nginx2.Warn {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"name":    name,
-		"content": content,
-	})
-
-}
-
-type EditConfigJson struct {
-	Content string `json:"content" binding:"required"`
-}
-
-func EditConfig(c *gin.Context) {
-	name := c.Param("name")
-	var request EditConfigJson
-	err := c.BindJSON(&request)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-	path := nginx2.GetConfPath("/", name)
-	content := request.Content
-
-	origContent, err := os.ReadFile(path)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	if content != "" && content != string(origContent) {
-		// model.CreateBackup(path)
-		err = os.WriteFile(path, []byte(content), 0644)
-		if err != nil {
-			api.ErrHandler(c, err)
-			return
-		}
-	}
-
-	output := nginx2.Reload()
-
-	if nginx2.GetLogLevel(output) >= nginx2.Warn {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	GetConfig(c)
-}

+ 51 - 0
api/config/get.go

@@ -0,0 +1,51 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/sashabaranov/go-openai"
+	"net/http"
+	"os"
+)
+
+func GetConfig(c *gin.Context) {
+	name := c.Param("name")
+	path := nginx.GetConfPath("/", name)
+
+	stat, err := os.Stat(path)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	content, err := os.ReadFile(path)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	g := query.ChatGPTLog
+	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if chatgpt.Content == nil {
+		chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
+	}
+
+	c.JSON(http.StatusOK, config.Config{
+		Name:            name,
+		Content:         string(content),
+		ChatGPTMessages: chatgpt.Content,
+		FilePath:        path,
+		ModifiedAt:      stat.ModTime(),
+	})
+}

+ 69 - 0
api/config/list.go

@@ -0,0 +1,69 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"os"
+)
+
+func GetConfigs(c *gin.Context) {
+	orderBy := c.Query("order_by")
+	sort := c.DefaultQuery("sort", "desc")
+	dir := c.DefaultQuery("dir", "/")
+
+	configFiles, err := os.ReadDir(nginx.GetConfPath(dir))
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	var configs []config.Config
+
+	for i := range configFiles {
+		file := configFiles[i]
+		fileInfo, _ := file.Info()
+
+		switch mode := fileInfo.Mode(); {
+		case mode.IsRegular(): // regular file, not a hidden file
+			if "." == file.Name()[0:1] {
+				continue
+			}
+		case mode&os.ModeSymlink != 0: // is a symbol
+			var targetPath string
+			targetPath, err = os.Readlink(nginx.GetConfPath(file.Name()))
+			if err != nil {
+				logger.Error("Read Symlink Error", targetPath, err)
+				continue
+			}
+
+			var targetInfo os.FileInfo
+			targetInfo, err = os.Stat(targetPath)
+			if err != nil {
+				logger.Error("Stat Error", targetPath, err)
+				continue
+			}
+			// but target file is not a dir
+			if targetInfo.IsDir() {
+				continue
+			}
+		}
+
+		configs = append(configs, config.Config{
+			Name:       file.Name(),
+			ModifiedAt: fileInfo.ModTime(),
+			Size:       fileInfo.Size(),
+			IsDir:      fileInfo.IsDir(),
+		})
+	}
+
+	configs = config.Sort(orderBy, sort, configs)
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": configs,
+	})
+}

+ 51 - 0
api/config/modify.go

@@ -0,0 +1,51 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"os"
+)
+
+type EditConfigJson struct {
+	Content string `json:"content" binding:"required"`
+}
+
+func EditConfig(c *gin.Context) {
+	name := c.Param("name")
+	var request EditConfigJson
+	err := c.BindJSON(&request)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	path := nginx.GetConfPath("/", name)
+	content := request.Content
+
+	origContent, err := os.ReadFile(path)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if content != "" && content != string(origContent) {
+		// model.CreateBackup(path)
+		err = os.WriteFile(path, []byte(content), 0644)
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+	}
+
+	output := nginx.Reload()
+
+	if nginx.GetLogLevel(output) >= nginx.Warn {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	GetConfig(c)
+}

+ 134 - 0
api/cosy/cosy.go

@@ -0,0 +1,134 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/gin-gonic/gin"
+	"github.com/go-playground/validator/v10"
+	"gorm.io/gorm"
+)
+
+var validate *validator.Validate
+
+func init() {
+	validate = validator.New()
+}
+
+type Ctx[T any] struct {
+	ctx                   *gin.Context
+	rules                 gin.H
+	Payload               map[string]interface{}
+	Model                 T
+	abort                 bool
+	nextHandler           *gin.HandlerFunc
+	beforeDecodeHookFunc  []func(ctx *Ctx[T])
+	beforeExecuteHookFunc []func(ctx *Ctx[T])
+	executedHookFunc      []func(ctx *Ctx[T])
+	gormScopes            []func(tx *gorm.DB) *gorm.DB
+	preloads              []string
+	scan                  func(tx *gorm.DB) any
+	transformer           func(*T) any
+	SelectedFields        []string
+}
+
+func Core[T any](c *gin.Context) *Ctx[T] {
+	return &Ctx[T]{
+		ctx:                   c,
+		gormScopes:            make([]func(tx *gorm.DB) *gorm.DB, 0),
+		beforeExecuteHookFunc: make([]func(ctx *Ctx[T]), 0),
+		beforeDecodeHookFunc:  make([]func(ctx *Ctx[T]), 0),
+	}
+}
+
+func (c *Ctx[T]) SetValidRules(rules gin.H) *Ctx[T] {
+	c.rules = rules
+
+	return c
+}
+
+func (c *Ctx[T]) BeforeDecodeHook(hook ...func(ctx *Ctx[T])) *Ctx[T] {
+	c.beforeDecodeHookFunc = append(c.beforeDecodeHookFunc, hook...)
+	return c
+}
+
+func (c *Ctx[T]) BeforeExecuteHook(hook ...func(ctx *Ctx[T])) *Ctx[T] {
+	c.beforeExecuteHookFunc = append(c.beforeExecuteHookFunc, hook...)
+	return c
+}
+
+func (c *Ctx[T]) ExecutedHook(hook ...func(ctx *Ctx[T])) *Ctx[T] {
+	c.executedHookFunc = append(c.executedHookFunc, hook...)
+	return c
+}
+
+func (c *Ctx[T]) SetPreloads(args ...string) *Ctx[T] {
+	c.preloads = append(c.preloads, args...)
+	return c
+}
+
+func (c *Ctx[T]) beforeExecuteHook() {
+	if len(c.beforeExecuteHookFunc) > 0 {
+		for _, v := range c.beforeExecuteHookFunc {
+			v(c)
+		}
+	}
+}
+
+func (c *Ctx[T]) beforeDecodeHook() {
+	if len(c.beforeDecodeHookFunc) > 0 {
+		for _, v := range c.beforeDecodeHookFunc {
+			v(c)
+		}
+	}
+}
+
+func (c *Ctx[T]) validate() (errs gin.H) {
+	c.Payload = make(gin.H)
+
+	_ = c.ctx.ShouldBindJSON(&c.Payload)
+
+	errs = validate.ValidateMap(c.Payload, c.rules)
+
+	if len(errs) > 0 {
+		logger.Debug(errs)
+		for k := range errs {
+			errs[k] = c.rules[k]
+		}
+		return
+	}
+	// Make sure that the key in c.Payload is also the key of rules
+	validated := make(map[string]interface{})
+
+	for k, v := range c.Payload {
+		if _, ok := c.rules[k]; ok {
+			validated[k] = v
+		}
+	}
+
+	c.Payload = validated
+
+	return
+}
+
+func (c *Ctx[T]) SetScan(scan func(tx *gorm.DB) any) *Ctx[T] {
+	c.scan = scan
+	return c
+}
+
+func (c *Ctx[T]) SetTransformer(t func(m *T) any) *Ctx[T] {
+	c.transformer = t
+	return c
+}
+
+func (c *Ctx[T]) AbortWithError(err error) {
+	c.abort = true
+	errHandler(c.ctx, err)
+}
+
+func (c *Ctx[T]) Abort() {
+	c.abort = true
+}
+
+func (c *Ctx[T]) GormScope(hook func(tx *gorm.DB) *gorm.DB) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, hook)
+	return c
+}

+ 72 - 0
api/cosy/create.go

@@ -0,0 +1,72 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/api/cosy/map2struct"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm/clause"
+	"net/http"
+)
+
+func (c *Ctx[T]) Create() {
+
+	errs := c.validate()
+
+	if len(errs) > 0 {
+		c.ctx.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "Requested with wrong parameters",
+			"errors":  errs,
+		})
+		return
+	}
+
+	db := model.UseDB()
+
+	c.beforeDecodeHook()
+
+	if c.abort {
+		return
+	}
+
+	err := map2struct.WeakDecode(c.Payload, &c.Model)
+
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	c.beforeExecuteHook()
+
+	if c.abort {
+		return
+	}
+
+	// skip all associations
+	err = db.Omit(clause.Associations).Create(&c.Model).Error
+
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	tx := db.Preload(clause.Associations)
+	for _, v := range c.preloads {
+		tx = tx.Preload(v)
+	}
+	tx.First(&c.Model)
+
+	if len(c.executedHookFunc) > 0 {
+		for _, v := range c.executedHookFunc {
+			v(c)
+
+			if c.abort {
+				return
+			}
+		}
+	}
+	if c.nextHandler != nil {
+		(*c.nextHandler)(c.ctx)
+	} else {
+		c.ctx.JSON(http.StatusOK, c.Model)
+	}
+}

+ 39 - 0
api/cosy/custom.go

@@ -0,0 +1,39 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/api/cosy/map2struct"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func (c *Ctx[T]) Custom(fx func(ctx *Ctx[T])) {
+	if c.abort {
+		return
+	}
+	errs := c.validate()
+
+	if len(errs) > 0 {
+		c.ctx.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "Requested with wrong parameters",
+			"errors":  errs,
+		})
+		return
+	}
+
+	c.beforeDecodeHook()
+
+	for k := range c.Payload {
+		c.SelectedFields = append(c.SelectedFields, k)
+	}
+
+	err := map2struct.WeakDecode(c.Payload, &c.Model)
+
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	c.beforeExecuteHook()
+
+	fx(c)
+}

+ 91 - 0
api/cosy/delete.go

@@ -0,0 +1,91 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/model"
+	"gorm.io/gorm"
+	"net/http"
+)
+
+func (c *Ctx[T]) Destroy() {
+	if c.abort {
+		return
+	}
+	id := c.ctx.Param("id")
+
+	c.beforeExecuteHook()
+
+	db := model.UseDB()
+	var dbModel T
+
+	result := db
+	if len(c.gormScopes) > 0 {
+		result = result.Scopes(c.gormScopes...)
+	}
+
+	err := result.Session(&gorm.Session{}).First(&dbModel, id).Error
+
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	err = result.Delete(&dbModel).Error
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	if len(c.executedHookFunc) > 0 {
+		for _, v := range c.executedHookFunc {
+			v(c)
+
+			if c.abort {
+				return
+			}
+		}
+	}
+
+	c.ctx.JSON(http.StatusNoContent, nil)
+}
+
+func (c *Ctx[T]) Recover() {
+	if c.abort {
+		return
+	}
+	id := c.ctx.Param("id")
+
+	c.beforeExecuteHook()
+
+	db := model.UseDB()
+	var dbModel T
+
+	result := db.Unscoped()
+	if len(c.gormScopes) > 0 {
+		result = result.Scopes(c.gormScopes...)
+	}
+
+	err := result.Session(&gorm.Session{}).First(&dbModel, id).Error
+
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	err = result.Model(&dbModel).Update("deleted_at", nil).Error
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	if len(c.executedHookFunc) > 0 {
+		for _, v := range c.executedHookFunc {
+			v(c)
+
+			if c.abort {
+				return
+			}
+		}
+	}
+
+	c.ctx.JSON(http.StatusNoContent, nil)
+}

+ 23 - 0
api/cosy/error.go

@@ -0,0 +1,23 @@
+package cosy
+
+import (
+	"errors"
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/gin-gonic/gin"
+	"go.uber.org/zap"
+	"gorm.io/gorm"
+	"net/http"
+)
+
+func errHandler(c *gin.Context, err error) {
+	logger.GetLogger().WithOptions(zap.AddCallerSkip(1)).Errorln(err)
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusInternalServerError, gin.H{
+		"message": err.Error(),
+	})
+}

+ 158 - 0
api/cosy/list.go

@@ -0,0 +1,158 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/spf13/cast"
+	"gorm.io/gorm"
+	"net/http"
+)
+
+func (c *Ctx[T]) SetFussy(keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToFussySearch(c.ctx, tx, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) SetFussyKeys(value string, keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToFussyKeysSearch(c.ctx, tx, value, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) SetEqual(keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToEqualSearch(c.ctx, tx, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) SetIn(keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToInSearch(c.ctx, tx, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) SetOrFussy(keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToOrFussySearch(c.ctx, tx, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) SetOrEqual(keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToOrEqualSearch(c.ctx, tx, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) SetOrIn(keys ...string) *Ctx[T] {
+	c.gormScopes = append(c.gormScopes, func(tx *gorm.DB) *gorm.DB {
+		return model.QueryToOrInSearch(c.ctx, tx, keys...)
+	})
+	return c
+}
+
+func (c *Ctx[T]) result() (*gorm.DB, bool) {
+	for _, v := range c.preloads {
+		t := v
+		c.GormScope(func(tx *gorm.DB) *gorm.DB {
+			tx = tx.Preload(t)
+			return tx
+		})
+	}
+
+	c.beforeExecuteHook()
+
+	var dbModel T
+	result := model.UseDB()
+
+	if c.ctx.Query("trash") == "true" {
+		stmt := &gorm.Statement{DB: model.UseDB()}
+		err := stmt.Parse(&dbModel)
+		if err != nil {
+			logger.Error(err)
+			return nil, false
+		}
+		result = result.Unscoped().Where(stmt.Schema.Table + ".deleted_at IS NOT NULL")
+	}
+
+	result = result.Model(&dbModel)
+
+	if len(c.gormScopes) > 0 {
+		result = result.Scopes(c.gormScopes...)
+	}
+
+	return result, true
+}
+
+func (c *Ctx[T]) ListAllData() ([]*T, bool) {
+	result, ok := c.result()
+	if !ok {
+		return nil, false
+	}
+
+	result = result.Scopes(model.SortOrder(c.ctx))
+	models := make([]*T, 0)
+	result.Find(&models)
+	return models, true
+}
+
+func (c *Ctx[T]) PagingListData() (*model.DataList, bool) {
+	result, ok := c.result()
+	if !ok {
+		return nil, false
+	}
+
+	result = result.Scopes(model.OrderAndPaginate(c.ctx))
+	data := &model.DataList{}
+	if c.scan == nil {
+		models := make([]*T, 0)
+		result.Find(&models)
+
+		if c.transformer != nil {
+			transformed := make([]any, 0)
+			for k := range models {
+				transformed = append(transformed, c.transformer(models[k]))
+			}
+			data.Data = transformed
+		} else {
+			data.Data = models
+		}
+	} else {
+		data.Data = c.scan(result)
+	}
+
+	page := cast.ToInt(c.ctx.Query("page"))
+	if page == 0 {
+		page = 1
+	}
+
+	pageSize := settings.AppSettings.PageSize
+	if reqPageSize := c.ctx.Query("page_size"); reqPageSize != "" {
+		pageSize = cast.ToInt(reqPageSize)
+	}
+
+	var totalRecords int64
+	result.Session(&gorm.Session{}).Count(&totalRecords)
+
+	data.Pagination = model.Pagination{
+		Total:       totalRecords,
+		PerPage:     pageSize,
+		CurrentPage: page,
+		TotalPages:  model.TotalPage(totalRecords, pageSize),
+	}
+	return data, true
+}
+
+func (c *Ctx[T]) PagingList() {
+	data, ok := c.PagingListData()
+	if ok {
+		c.ctx.JSON(http.StatusOK, data)
+	}
+}

+ 56 - 0
api/cosy/map2struct/hook.go

@@ -0,0 +1,56 @@
+package map2struct
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/shopspring/decimal"
+	"github.com/spf13/cast"
+	"reflect"
+	"time"
+)
+
+var timeLocation *time.Location
+
+func init() {
+	timeLocation = time.Local
+}
+
+func ToTimeHookFunc() mapstructure.DecodeHookFunc {
+	return func(
+		f reflect.Type,
+		t reflect.Type,
+		data interface{}) (interface{}, error) {
+		if t != reflect.TypeOf(time.Time{}) {
+			return data, nil
+		}
+
+		switch f.Kind() {
+		case reflect.String:
+			return cast.ToTimeInDefaultLocationE(data, timeLocation)
+		case reflect.Float64:
+			return time.Unix(0, int64(data.(float64))*int64(time.Millisecond)), nil
+		case reflect.Int64:
+			return time.Unix(0, data.(int64)*int64(time.Millisecond)), nil
+		default:
+			return data, nil
+		}
+		// Convert it by parsing
+	}
+}
+
+func ToDecimalHookFunc() mapstructure.DecodeHookFunc {
+	return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
+
+		if t == reflect.TypeOf(decimal.Decimal{}) {
+			if f.Kind() == reflect.Float64 {
+				return decimal.NewFromFloat(data.(float64)), nil
+			}
+
+			if input := data.(string); input != "" {
+				return decimal.NewFromString(data.(string))
+			}
+			return decimal.Decimal{}, nil
+		}
+
+		return data, nil
+	}
+}

+ 24 - 0
api/cosy/map2struct/map2struct.go

@@ -0,0 +1,24 @@
+package map2struct
+
+import (
+	"github.com/mitchellh/mapstructure"
+)
+
+func WeakDecode(input, output interface{}) error {
+	config := &mapstructure.DecoderConfig{
+		Metadata:         nil,
+		Result:           output,
+		WeaklyTypedInput: true,
+		DecodeHook: mapstructure.ComposeDecodeHookFunc(
+			ToDecimalHookFunc(), ToTimeHookFunc(),
+		),
+		TagName: "json",
+	}
+
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return err
+	}
+
+	return decoder.Decode(input)
+}

+ 42 - 0
api/cosy/order.go

@@ -0,0 +1,42 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/model"
+	"gorm.io/gorm"
+	"net/http"
+)
+
+func (c *Ctx[T]) UpdateOrder() {
+	var json struct {
+		TargetID    int   `json:"target_id"`
+		Direction   int   `json:"direction" binding:"oneof=-1 1"`
+		AffectedIDs []int `json:"affected_ids"`
+	}
+
+	if !api.BindAndValid(c.ctx, &json) {
+		return
+	}
+
+	affectedLen := len(json.AffectedIDs)
+
+	db := model.UseDB()
+
+	// update target
+	err := db.Model(&c.Model).Where("id = ?", json.TargetID).Update("order_id", gorm.Expr("order_id + ?", affectedLen*(-json.Direction))).Error
+
+	if err != nil {
+		api.ErrHandler(c.ctx, err)
+		return
+	}
+
+	// update affected
+	err = db.Model(&c.Model).Where("id in ?", json.AffectedIDs).Update("order_id", gorm.Expr("order_id + ?", json.Direction)).Error
+
+	if err != nil {
+		api.ErrHandler(c.ctx, err)
+		return
+	}
+
+	c.ctx.JSON(http.StatusOK, json)
+}

+ 90 - 0
api/cosy/update.go

@@ -0,0 +1,90 @@
+package cosy
+
+import (
+	"github.com/0xJacky/Nginx-UI/api/cosy/map2struct"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/gin-gonic/gin"
+	"gorm.io/gorm"
+	"net/http"
+)
+
+func (c *Ctx[T]) SetNextHandler(handler gin.HandlerFunc) *Ctx[T] {
+	c.nextHandler = &handler
+	return c
+}
+
+func (c *Ctx[T]) Modify() {
+	if c.abort {
+		return
+	}
+	id := c.ctx.Param("id")
+	errs := c.validate()
+
+	if len(errs) > 0 {
+		c.ctx.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "Requested with wrong parameters",
+			"errors":  errs,
+		})
+		return
+	}
+
+	db := model.UseDB()
+
+	result := db
+	if len(c.gormScopes) > 0 {
+		result = result.Scopes(c.gormScopes...)
+	}
+
+	err := result.Session(&gorm.Session{}).First(&c.Model, id).Error
+
+	if err != nil {
+		c.AbortWithError(err)
+		return
+	}
+
+	c.beforeDecodeHook()
+	if c.abort {
+		return
+	}
+
+	var selectedFields []string
+
+	for k := range c.Payload {
+		selectedFields = append(selectedFields, k)
+	}
+
+	err = map2struct.WeakDecode(c.Payload, &c.Model)
+
+	if err != nil {
+		errHandler(c.ctx, err)
+		return
+	}
+
+	c.beforeExecuteHook()
+	if c.abort {
+		return
+	}
+
+	err = db.Model(&c.Model).Select(selectedFields).Updates(&c.Model).Error
+
+	if err != nil {
+		c.AbortWithError(err)
+		return
+	}
+
+	if len(c.executedHookFunc) > 0 {
+		for _, v := range c.executedHookFunc {
+			v(c)
+
+			if c.abort {
+				return
+			}
+		}
+	}
+
+	if c.nextHandler != nil {
+		(*c.nextHandler)(c.ctx)
+	} else {
+		c.ctx.JSON(http.StatusOK, c.Model)
+	}
+}

+ 5 - 6
api/nginx/nginx.go

@@ -2,14 +2,14 @@ package nginx
 
 import (
 	"github.com/0xJacky/Nginx-UI/api"
-	nginx2 "github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/gin-gonic/gin"
 	"net/http"
 	"os"
 )
 
 func BuildNginxConfig(c *gin.Context) {
-	var ngxConf nginx2.NgxConfig
+	var ngxConf nginx.NgxConfig
 	if !api.BindAndValid(c, &ngxConf) {
 		return
 	}
@@ -29,7 +29,7 @@ func TokenizeNginxConfig(c *gin.Context) {
 	}
 
 	c.Set("maybe_error", "nginx_config_syntax_error")
-	ngxConfig := nginx2.ParseNgxConfigByContent(json.Content)
+	ngxConfig := nginx.ParseNgxConfigByContent(json.Content)
 
 	c.JSON(http.StatusOK, ngxConfig)
 
@@ -46,12 +46,12 @@ func FormatNginxConfig(c *gin.Context) {
 
 	c.Set("maybe_error", "nginx_config_syntax_error")
 	c.JSON(http.StatusOK, gin.H{
-		"content": nginx2.FmtCode(json.Content),
+		"content": nginx.FmtCode(json.Content),
 	})
 }
 
 func Status(c *gin.Context) {
-	pidPath := nginx2.GetNginxPIDPath()
+	pidPath := nginx.GetNginxPIDPath()
 
 	running := true
 	if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 { // fileInfo.Size() == 0 no process id
@@ -62,4 +62,3 @@ func Status(c *gin.Context) {
 		"running": running,
 	})
 }
-

+ 5 - 40
api/openai/openai.go

@@ -2,10 +2,9 @@ package openai
 
 import (
 	"context"
+	"crypto/tls"
 	"fmt"
 	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/model"
-	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/gin-gonic/gin"
 	"github.com/pkg/errors"
@@ -35,7 +34,7 @@ func MakeChatCompletionRequest(c *gin.Context) {
 	}
 	messages = append(messages, json.Messages...)
 	// sse server
-	c.Writer.Header().Set("Content-Type", "text/event-stream")
+	c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
 	c.Writer.Header().Set("Cache-Control", "no-cache")
 	c.Writer.Header().Set("Connection", "keep-alive")
 	c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
@@ -66,7 +65,8 @@ func MakeChatCompletionRequest(c *gin.Context) {
 			return
 		}
 		transport := &http.Transport{
-			Proxy: http.ProxyURL(proxyUrl),
+			Proxy:           http.ProxyURL(proxyUrl),
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
 		}
 		config.HTTPClient = &http.Client{
 			Transport: transport,
@@ -100,17 +100,16 @@ func MakeChatCompletionRequest(c *gin.Context) {
 	defer stream.Close()
 	msgChan := make(chan string)
 	go func() {
+		defer close(msgChan)
 		for {
 			response, err := stream.Recv()
 			if errors.Is(err, io.EOF) {
-				close(msgChan)
 				fmt.Println()
 				return
 			}
 
 			if err != nil {
 				fmt.Printf("Stream error: %v\n", err)
-				close(msgChan)
 				return
 			}
 
@@ -133,37 +132,3 @@ func MakeChatCompletionRequest(c *gin.Context) {
 		return false
 	})
 }
-
-func StoreChatGPTRecord(c *gin.Context) {
-	var json struct {
-		FileName string                         `json:"file_name"`
-		Messages []openai.ChatCompletionMessage `json:"messages"`
-	}
-
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-
-	name := json.FileName
-	g := query.ChatGPTLog
-	_, err := g.Where(g.Name.Eq(name)).FirstOrCreate()
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	_, err = g.Where(g.Name.Eq(name)).Updates(&model.ChatGPTLog{
-		Name:    name,
-		Content: json.Messages,
-	})
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
-}

+ 44 - 0
api/openai/store.go

@@ -0,0 +1,44 @@
+package openai
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"github.com/sashabaranov/go-openai"
+	"net/http"
+)
+
+func StoreChatGPTRecord(c *gin.Context) {
+	var json struct {
+		FileName string                         `json:"file_name"`
+		Messages []openai.ChatCompletionMessage `json:"messages"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	name := json.FileName
+	g := query.ChatGPTLog
+	_, err := g.Where(g.Name.Eq(name)).FirstOrCreate()
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	_, err = g.Where(g.Name.Eq(name)).Updates(&model.ChatGPTLog{
+		Name:    name,
+		Content: json.Messages,
+	})
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}

+ 42 - 0
api/sites/advance.go

@@ -0,0 +1,42 @@
+package sites
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func DomainEditByAdvancedMode(c *gin.Context) {
+	var json struct {
+		Advanced bool `json:"advanced"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	name := c.Param("name")
+	path := nginx.GetConfPath("sites-available", name)
+
+	s := query.Site
+
+	_, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+
+}

+ 64 - 0
api/sites/auto_cert.go

@@ -0,0 +1,64 @@
+package sites
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func AddDomainToAutoCert(c *gin.Context) {
+	name := c.Param("name")
+
+	var json struct {
+		DnsCredentialID int      `json:"dns_credential_id"`
+		ChallengeMethod string   `json:"challenge_method"`
+		Domains         []string `json:"domains"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	certModel, err := model.FirstOrCreateCert(name)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	err = certModel.Updates(&model.Cert{
+		Name:            name,
+		Domains:         json.Domains,
+		AutoCert:        model.AutoCertEnabled,
+		DnsCredentialID: json.DnsCredentialID,
+		ChallengeMethod: json.ChallengeMethod,
+	})
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, certModel)
+}
+
+func RemoveDomainFromAutoCert(c *gin.Context) {
+	name := c.Param("name")
+	certModel, err := model.FirstCert(name)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	err = certModel.Updates(&model.Cert{
+		AutoCert: model.AutoCertDisabled,
+	})
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, nil)
+}

+ 70 - 206
api/sites/domain.go

@@ -1,20 +1,19 @@
 package sites
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/internal/cert"
-	"github.com/0xJacky/Nginx-UI/internal/config_list"
-	helper2 "github.com/0xJacky/Nginx-UI/internal/helper"
-	"github.com/0xJacky/Nginx-UI/internal/logger"
-	nginx2 "github.com/0xJacky/Nginx-UI/internal/nginx"
-	"github.com/0xJacky/Nginx-UI/model"
-	"github.com/0xJacky/Nginx-UI/query"
-	"github.com/gin-gonic/gin"
-	"github.com/sashabaranov/go-openai"
-	"net/http"
-	"os"
-	"strings"
-	"time"
+    "github.com/0xJacky/Nginx-UI/api"
+    "github.com/0xJacky/Nginx-UI/internal/cert"
+    "github.com/0xJacky/Nginx-UI/internal/config"
+    "github.com/0xJacky/Nginx-UI/internal/helper"
+    "github.com/0xJacky/Nginx-UI/internal/logger"
+    "github.com/0xJacky/Nginx-UI/internal/nginx"
+    "github.com/0xJacky/Nginx-UI/model"
+    "github.com/0xJacky/Nginx-UI/query"
+    "github.com/gin-gonic/gin"
+    "github.com/sashabaranov/go-openai"
+    "net/http"
+    "os"
+    "strings"
 )
 
 func GetDomains(c *gin.Context) {
@@ -22,20 +21,14 @@ func GetDomains(c *gin.Context) {
 	orderBy := c.Query("order_by")
 	sort := c.DefaultQuery("sort", "desc")
 
-	mySort := map[string]string{
-		"enabled": "bool",
-		"name":    "string",
-		"modify":  "time",
-	}
-
-	configFiles, err := os.ReadDir(nginx2.GetConfPath("sites-available"))
+	configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
 
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
 
-	enabledConfig, err := os.ReadDir(nginx2.GetConfPath("sites-enabled"))
+	enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
 
 	if err != nil {
 		api.ErrHandler(c, err)
@@ -47,7 +40,7 @@ func GetDomains(c *gin.Context) {
 		enabledConfigMap[enabledConfig[i].Name()] = true
 	}
 
-	var configs []gin.H
+	var configs []config.Config
 
 	for i := range configFiles {
 		file := configFiles[i]
@@ -56,29 +49,23 @@ func GetDomains(c *gin.Context) {
 			if name != "" && !strings.Contains(file.Name(), name) {
 				continue
 			}
-			configs = append(configs, gin.H{
-				"name":    file.Name(),
-				"size":    fileInfo.Size(),
-				"modify":  fileInfo.ModTime(),
-				"enabled": enabledConfigMap[file.Name()],
+			configs = append(configs, config.Config{
+				Name:       file.Name(),
+				ModifiedAt: fileInfo.ModTime(),
+				Size:       fileInfo.Size(),
+				IsDir:      fileInfo.IsDir(),
+				Enabled:    enabledConfigMap[file.Name()],
 			})
 		}
 	}
 
-	configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
+	configs = config.Sort(orderBy, sort, configs)
 
 	c.JSON(http.StatusOK, gin.H{
 		"data": configs,
 	})
 }
 
-type CertificateInfo struct {
-	SubjectName string    `json:"subject_name"`
-	IssuerName  string    `json:"issuer_name"`
-	NotAfter    time.Time `json:"not_after"`
-	NotBefore   time.Time `json:"not_before"`
-}
-
 func GetDomain(c *gin.Context) {
 	rewriteName, ok := c.Get("rewriteConfigFileName")
 
@@ -89,7 +76,7 @@ func GetDomain(c *gin.Context) {
 		name = rewriteName.(string)
 	}
 
-	path := nginx2.GetConfPath("sites-available", name)
+	path := nginx.GetConfPath("sites-available", name)
 	file, err := os.Stat(path)
 	if os.IsNotExist(err) {
 		c.JSON(http.StatusNotFound, gin.H{
@@ -100,7 +87,7 @@ func GetDomain(c *gin.Context) {
 
 	enabled := true
 
-	if _, err := os.Stat(nginx2.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
+	if _, err := os.Stat(nginx.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
 		enabled = false
 	}
 
@@ -127,7 +114,7 @@ func GetDomain(c *gin.Context) {
 	certModel, err := model.FirstCert(name)
 
 	if err != nil {
-		logger.Warn("cert", err)
+		logger.Warn(err)
 	}
 
 	if site.Advanced {
@@ -137,20 +124,20 @@ func GetDomain(c *gin.Context) {
 			return
 		}
 
-		c.JSON(http.StatusOK, gin.H{
-			"modified_at":      file.ModTime(),
-			"advanced":         site.Advanced,
-			"enabled":          enabled,
-			"name":             name,
-			"config":           string(origContent),
-			"auto_cert":        certModel.AutoCert == model.AutoCertEnabled,
-			"chatgpt_messages": chatgpt.Content,
+		c.JSON(http.StatusOK, Site{
+			ModifiedAt:      file.ModTime(),
+			Advanced:        site.Advanced,
+			Enabled:         enabled,
+			Name:            name,
+			Config:          string(origContent),
+			AutoCert:        certModel.AutoCert == model.AutoCertEnabled,
+			ChatGPTMessages: chatgpt.Content,
 		})
 		return
 	}
 
 	c.Set("maybe_error", "nginx_config_syntax_error")
-	config, err := nginx2.ParseNgxConfig(path)
+	nginxConfig, err := nginx.ParseNgxConfig(path)
 
 	if err != nil {
 		api.ErrHandler(c, err)
@@ -160,7 +147,7 @@ func GetDomain(c *gin.Context) {
 	c.Set("maybe_error", "")
 
 	certInfoMap := make(map[int]CertificateInfo)
-	for serverIdx, server := range config.Servers {
+	for serverIdx, server := range nginxConfig.Servers {
 		for _, directive := range server.Directives {
 			if directive.Directive == "ssl_certificate" {
 
@@ -185,18 +172,17 @@ func GetDomain(c *gin.Context) {
 
 	c.Set("maybe_error", "nginx_config_syntax_error")
 
-	c.JSON(http.StatusOK, gin.H{
-		"modified_at":      file.ModTime(),
-		"advanced":         site.Advanced,
-		"enabled":          enabled,
-		"name":             name,
-		"config":           config.FmtCode(),
-		"tokenized":        config,
-		"auto_cert":        certModel.AutoCert == model.AutoCertEnabled,
-		"cert_info":        certInfoMap,
-		"chatgpt_messages": chatgpt.Content,
+	c.JSON(http.StatusOK, Site{
+		ModifiedAt:      file.ModTime(),
+		Advanced:        site.Advanced,
+		Enabled:         enabled,
+		Name:            name,
+		Config:          nginxConfig.FmtCode(),
+		Tokenized:       nginxConfig,
+		AutoCert:        certModel.AutoCert == model.AutoCertEnabled,
+		CertInfo:        certInfoMap,
+		ChatGPTMessages: chatgpt.Content,
 	})
-
 }
 
 func SaveDomain(c *gin.Context) {
@@ -219,9 +205,9 @@ func SaveDomain(c *gin.Context) {
 		return
 	}
 
-	path := nginx2.GetConfPath("sites-available", name)
+	path := nginx.GetConfPath("sites-available", name)
 
-	if !json.Overwrite && helper2.FileExists(path) {
+	if !json.Overwrite && helper.FileExists(path) {
 		c.JSON(http.StatusNotAcceptable, gin.H{
 			"message": "File exists",
 		})
@@ -233,24 +219,24 @@ func SaveDomain(c *gin.Context) {
 		api.ErrHandler(c, err)
 		return
 	}
-	enabledConfigFilePath := nginx2.GetConfPath("sites-enabled", name)
+	enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
 	// rename the config file if needed
 	if name != json.Name {
-		newPath := nginx2.GetConfPath("sites-available", json.Name)
+		newPath := nginx.GetConfPath("sites-available", json.Name)
 		s := query.Site
 		_, err = s.Where(s.Path.Eq(path)).Update(s.Path, newPath)
 
 		// check if dst file exists, do not rename
-		if helper2.FileExists(newPath) {
+		if helper.FileExists(newPath) {
 			c.JSON(http.StatusNotAcceptable, gin.H{
 				"message": "File exists",
 			})
 			return
 		}
 		// recreate soft link
-		if helper2.FileExists(enabledConfigFilePath) {
+		if helper.FileExists(enabledConfigFilePath) {
 			_ = os.Remove(enabledConfigFilePath)
-			enabledConfigFilePath = nginx2.GetConfPath("sites-enabled", json.Name)
+			enabledConfigFilePath = nginx.GetConfPath("sites-enabled", json.Name)
 			err = os.Symlink(newPath, enabledConfigFilePath)
 
 			if err != nil {
@@ -269,12 +255,12 @@ func SaveDomain(c *gin.Context) {
 		c.Set("rewriteConfigFileName", name)
 	}
 
-	enabledConfigFilePath = nginx2.GetConfPath("sites-enabled", name)
-	if helper2.FileExists(enabledConfigFilePath) {
+	enabledConfigFilePath = nginx.GetConfPath("sites-enabled", name)
+	if helper.FileExists(enabledConfigFilePath) {
 		// Test nginx configuration
-		output := nginx2.TestConf()
+		output := nginx.TestConf()
 
-		if nginx2.GetLogLevel(output) > nginx2.Warn {
+		if nginx.GetLogLevel(output) > nginx.Warn {
 			c.JSON(http.StatusInternalServerError, gin.H{
 				"message": output,
 				"error":   "nginx_config_syntax_error",
@@ -282,9 +268,9 @@ func SaveDomain(c *gin.Context) {
 			return
 		}
 
-		output = nginx2.Reload()
+		output = nginx.Reload()
 
-		if nginx2.GetLogLevel(output) > nginx2.Warn {
+		if nginx.GetLogLevel(output) > nginx.Warn {
 			c.JSON(http.StatusInternalServerError, gin.H{
 				"message": output,
 			})
@@ -296,8 +282,8 @@ func SaveDomain(c *gin.Context) {
 }
 
 func EnableDomain(c *gin.Context) {
-	configFilePath := nginx2.GetConfPath("sites-available", c.Param("name"))
-	enabledConfigFilePath := nginx2.GetConfPath("sites-enabled", c.Param("name"))
+	configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
+	enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
 
 	_, err := os.Stat(configFilePath)
 
@@ -316,9 +302,9 @@ func EnableDomain(c *gin.Context) {
 	}
 
 	// Test nginx config, if not pass then disable the site.
-	output := nginx2.TestConf()
+	output := nginx.TestConf()
 
-	if nginx2.GetLogLevel(output) > nginx2.Warn {
+	if nginx.GetLogLevel(output) > nginx.Warn {
 		_ = os.Remove(enabledConfigFilePath)
 		c.JSON(http.StatusInternalServerError, gin.H{
 			"message": output,
@@ -326,9 +312,9 @@ func EnableDomain(c *gin.Context) {
 		return
 	}
 
-	output = nginx2.Reload()
+	output = nginx.Reload()
 
-	if nginx2.GetLogLevel(output) > nginx2.Warn {
+	if nginx.GetLogLevel(output) > nginx.Warn {
 		c.JSON(http.StatusInternalServerError, gin.H{
 			"message": output,
 		})
@@ -341,7 +327,7 @@ func EnableDomain(c *gin.Context) {
 }
 
 func DisableDomain(c *gin.Context) {
-	enabledConfigFilePath := nginx2.GetConfPath("sites-enabled", c.Param("name"))
+	enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
 
 	_, err := os.Stat(enabledConfigFilePath)
 
@@ -365,9 +351,9 @@ func DisableDomain(c *gin.Context) {
 		return
 	}
 
-	output := nginx2.Reload()
+	output := nginx.Reload()
 
-	if nginx2.GetLogLevel(output) > nginx2.Warn {
+	if nginx.GetLogLevel(output) > nginx.Warn {
 		c.JSON(http.StatusInternalServerError, gin.H{
 			"message": output,
 		})
@@ -382,8 +368,8 @@ func DisableDomain(c *gin.Context) {
 func DeleteDomain(c *gin.Context) {
 	var err error
 	name := c.Param("name")
-	availablePath := nginx2.GetConfPath("sites-available", name)
-	enabledPath := nginx2.GetConfPath("sites-enabled", name)
+	availablePath := nginx.GetConfPath("sites-available", name)
+	enabledPath := nginx.GetConfPath("sites-enabled", name)
 
 	if _, err = os.Stat(availablePath); os.IsNotExist(err) {
 		c.JSON(http.StatusNotFound, gin.H{
@@ -412,126 +398,4 @@ func DeleteDomain(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
-
-}
-
-func AddDomainToAutoCert(c *gin.Context) {
-	name := c.Param("name")
-
-	var json struct {
-		model.Cert
-		Domains []string `json:"domains"`
-	}
-
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-
-	certModel, err := model.FirstOrCreateCert(name)
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	err = certModel.Updates(&model.Cert{
-		Name:            name,
-		Domains:         json.Domains,
-		AutoCert:        model.AutoCertEnabled,
-		DnsCredentialID: json.DnsCredentialID,
-		ChallengeMethod: json.ChallengeMethod,
-	})
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	c.JSON(http.StatusOK, certModel)
-}
-
-func RemoveDomainFromAutoCert(c *gin.Context) {
-	name := c.Param("name")
-	certModel, err := model.FirstCert(name)
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	err = certModel.Updates(&model.Cert{
-		AutoCert: model.AutoCertDisabled,
-	})
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-	c.JSON(http.StatusOK, nil)
-}
-
-func DuplicateSite(c *gin.Context) {
-	name := c.Param("name")
-
-	var json struct {
-		Name string `json:"name" binding:"required"`
-	}
-
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-
-	src := nginx2.GetConfPath("sites-available", name)
-	dst := nginx2.GetConfPath("sites-available", json.Name)
-
-	if helper2.FileExists(dst) {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "File exists",
-		})
-		return
-	}
-
-	_, err := helper2.CopyFile(src, dst)
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"dst": dst,
-	})
-}
-
-func DomainEditByAdvancedMode(c *gin.Context) {
-	var json struct {
-		Advanced bool `json:"advanced"`
-	}
-
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-
-	name := c.Param("name")
-	path := nginx2.GetConfPath("sites-available", name)
-
-	s := query.Site
-
-	_, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
-
 }

+ 44 - 0
api/sites/duplicate.go

@@ -0,0 +1,44 @@
+package sites
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func DuplicateSite(c *gin.Context) {
+	// Source name
+	name := c.Param("name")
+
+	// Destination name
+	var json struct {
+		Name string `json:"name" binding:"required"`
+	}
+
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	src := nginx.GetConfPath("sites-available", name)
+	dst := nginx.GetConfPath("sites-available", json.Name)
+
+	if helper.FileExists(dst) {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "File exists",
+		})
+		return
+	}
+
+	_, err := helper.CopyFile(src, dst)
+
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"dst": dst,
+	})
+}

+ 26 - 0
api/sites/sites.go

@@ -0,0 +1,26 @@
+package sites
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/sashabaranov/go-openai"
+	"time"
+)
+
+type CertificateInfo struct {
+	SubjectName string    `json:"subject_name"`
+	IssuerName  string    `json:"issuer_name"`
+	NotAfter    time.Time `json:"not_after"`
+	NotBefore   time.Time `json:"not_before"`
+}
+
+type Site struct {
+	ModifiedAt      time.Time                      `json:"modified_at"`
+	Advanced        bool                           `json:"advanced"`
+	Enabled         bool                           `json:"enabled"`
+	Name            string                         `json:"name"`
+	Config          string                         `json:"config"`
+	AutoCert        bool                           `json:"auto_cert"`
+	ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
+	Tokenized       *nginx.NgxConfig               `json:"tokenized,omitempty"`
+	CertInfo        map[int]CertificateInfo        `json:"cert_info,omitempty"`
+}

+ 1 - 1
api/template/template.go

@@ -76,7 +76,7 @@ func GetTemplateBlock(c *gin.Context) {
 		template.ConfigInfoItem
 		template.ConfigDetail
 	}
-	var bindData map[string]template.TVariable
+	var bindData map[string]template.Variable
 	_ = c.ShouldBindJSON(&bindData)
 	info := template.GetTemplateInfo("block", c.Param("name"))
 

+ 258 - 0
app/.eslintrc.js

@@ -0,0 +1,258 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+  },
+  extends: [
+    '@antfu/eslint-config-vue',
+    'plugin:vue/vue3-recommended',
+    'plugin:import/recommended',
+    'plugin:import/typescript',
+    'plugin:promise/recommended',
+    'plugin:sonarjs/recommended',
+    'plugin:@typescript-eslint/recommended',
+
+    // 'plugin:unicorn/recommended',
+  ],
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 13,
+    parser: '@typescript-eslint/parser',
+    sourceType: 'module',
+  },
+  plugins: [
+    'vue',
+    '@typescript-eslint',
+    'regex',
+  ],
+  ignorePatterns: ['src/@iconify/*.js', 'node_modules', 'dist', '*.d.ts'],
+  rules: {
+    'vue/no-v-html': 'off',
+
+    'vue/block-tag-newline': 'off',
+    // eslint-disable-next-line n/prefer-global/process
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    // eslint-disable-next-line n/prefer-global/process
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+
+    // indentation (Already present in TypeScript)
+    'comma-spacing': ['error', {
+      before: false,
+      after: true,
+    }],
+    'key-spacing': ['error', { afterColon: true }],
+
+    'vue/first-attribute-linebreak': ['error', {
+      singleline: 'beside',
+      multiline: 'below',
+    }],
+
+    'antfu/top-level-function': 'off',
+
+    // Enforce trailing comma (Already present in TypeScript)
+    'comma-dangle': ['error', 'always-multiline'],
+
+    // Disable max-len
+    'max-len': 'off',
+
+    // we don't want it
+    'semi': ['error', 'never'],
+
+    // add parens ony when required in arrow function
+    'arrow-parens': ['error', 'as-needed'],
+
+    // add new line above comment
+    'newline-before-return': 'error',
+
+    // add new line above comment
+    'lines-around-comment': [
+      'error',
+      {
+        beforeBlockComment: true,
+        beforeLineComment: true,
+        allowBlockStart: true,
+        allowClassStart: true,
+        allowObjectStart: true,
+        allowArrayStart: true,
+      },
+    ],
+
+    // Ignore _ as unused variable
+    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_+$' }],
+
+    'array-element-newline': ['error', 'consistent'],
+    'array-bracket-newline': ['error', 'consistent'],
+
+    'vue/multi-word-component-names': 'off',
+
+    'padding-line-between-statements': [
+      'error',
+      {
+        blankLine: 'always',
+        prev: 'expression',
+        next: 'const',
+      },
+      {
+        blankLine: 'always',
+        prev: 'const',
+        next: 'expression',
+      },
+      {
+        blankLine: 'always',
+        prev: 'multiline-const',
+        next: '*',
+      },
+      {
+        blankLine: 'always',
+        prev: '*',
+        next: 'multiline-const',
+      },
+    ],
+
+    // Plugin: eslint-plugin-import
+    'import/prefer-default-export': 'off',
+    'import/newline-after-import': ['error', { count: 1 }],
+    'no-restricted-imports': ['error', 'vuetify/components'],
+
+    // For omitting extension for ts files
+    'import/extensions': [
+      'error',
+      'ignorePackages',
+      {
+        mjs: 'never',
+        js: 'never',
+        jsx: 'never',
+        ts: 'never',
+        tsx: 'never',
+      },
+    ],
+
+    // ignore virtual files
+    'import/no-unresolved': [2, {
+      ignore: [
+        '~pages$',
+        'virtual:generated-layouts',
+
+        // Ignore vite's ?raw imports
+        '.*\?raw',
+      ],
+    }],
+
+    // Thanks: https://stackoverflow.com/a/63961972/10796681
+    'no-shadow': 'off',
+    '@typescript-eslint/no-shadow': ['error'],
+
+    '@typescript-eslint/consistent-type-imports': 'error',
+
+    // Plugin: eslint-plugin-promise
+    'promise/always-return': 'off',
+    'promise/catch-or-return': 'off',
+
+    // ESLint plugin vue
+    'vue/component-api-style': 'error',
+    'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }],
+    'vue/custom-event-name-casing': ['error', 'camelCase', {
+      ignores: [
+        '/^(click):[a-z]+((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?/',
+      ],
+    }],
+    'vue/define-macros-order': 'error',
+    'vue/html-comment-content-newline': 'error',
+    'vue/html-comment-content-spacing': 'error',
+    'vue/html-comment-indent': 'error',
+    'vue/match-component-file-name': 'error',
+    'vue/no-child-content': 'error',
+    'vue/require-default-prop': 'off',
+
+    // NOTE this rule only supported in SFC,  Users of the unplugin-vue-define-options should disable that rule: https://github.com/vuejs/eslint-plugin-vue/issues/1886
+    // 'vue/no-duplicate-attr-inheritance': 'error',
+    'vue/no-multiple-objects-in-class': 'error',
+    'vue/no-reserved-component-names': 'error',
+    'vue/no-template-target-blank': 'error',
+    'vue/no-useless-mustaches': 'error',
+    'vue/no-useless-v-bind': 'error',
+    'vue/padding-line-between-blocks': 'error',
+    'vue/prefer-separate-static-class': 'error',
+    'vue/prefer-true-attribute-shorthand': 'error',
+    'vue/v-on-function-call': 'error',
+    'vue/valid-v-slot': ['error', {
+      allowModifiers: true,
+    }],
+
+    // -- Extension Rules
+    'vue/no-irregular-whitespace': 'error',
+
+    // -- Sonarlint
+    'sonarjs/no-duplicate-string': 'off',
+    'sonarjs/no-nested-template-literals': 'off',
+
+    // -- Unicorn
+    // 'unicorn/filename-case': 'off',
+    // 'unicorn/prevent-abbreviations': ['error', {
+    //   replacements: {
+    //     props: false,
+    //   },
+    // }],
+    // https://github.com/gmullerb/eslint-plugin-regex
+    'regex/invalid': [
+      'error',
+      [
+        {
+          regex: '@/assets/images',
+          replacement: '@images',
+          message: 'Use \'@images\' path alias for image imports',
+        },
+        {
+          regex: '@/styles',
+          replacement: '@styles',
+          message: 'Use \'@styles\' path alias for importing styles from \'src/styles\'',
+        },
+
+        // {
+        //   id: 'Disallow icon of icon library',
+        //   regex: 'tabler-\\w',
+        //   message: 'Only \'mdi\' icons are allowed',
+        // },
+
+        {
+          regex: '@core/\\w',
+          message: 'You can\'t use @core when you are in @layouts module',
+          files: {
+            inspect: '@layouts/.*',
+          },
+        },
+        {
+          regex: 'useLayouts\\(',
+          message: '`useLayouts` composable is only allowed in @layouts & @core directory. Please use `useThemeConfig` composable instead.',
+          files: {
+            inspect: '^(?!.*(@core|@layouts)).*',
+          },
+        },
+      ],
+
+      // Ignore files
+      '\.eslintrc\.js',
+    ],
+  },
+  settings: {
+    'import/resolver': {
+      node: {
+        extensions: ['.ts', '.js', '.tsx', '.jsx', '.mjs', '.png', '.jpg'],
+      },
+      typescript: {},
+      alias: {
+        map: [
+          ['@', './src'],
+        ],
+      },
+    },
+  },
+  overrides: [
+    {
+      files: ['*.json'],
+      rules: {
+        'no-invalid-meta': 'off',
+      },
+    },
+  ],
+}

+ 16 - 8
app/components.d.ts

@@ -77,14 +77,22 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SetLanguageSetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
-    StdDataDisplayStdBatchEdit: typeof import('./src/components/StdDataDisplay/StdBatchEdit.vue')['default']
-    StdDataDisplayStdCurd: typeof import('./src/components/StdDataDisplay/StdCurd.vue')['default']
-    StdDataDisplayStdPagination: typeof import('./src/components/StdDataDisplay/StdPagination.vue')['default']
-    StdDataDisplayStdTable: typeof import('./src/components/StdDataDisplay/StdTable.vue')['default']
-    StdDataEntryComponentsStdPassword: typeof import('./src/components/StdDataEntry/components/StdPassword.vue')['default']
-    StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDataEntry/components/StdSelect.vue')['default']
-    StdDataEntryComponentsStdSelector: typeof import('./src/components/StdDataEntry/components/StdSelector.vue')['default']
-    StdDataEntryStdFormItem: typeof import('./src/components/StdDataEntry/StdFormItem.vue')['default']
+    StdDataDisplayStdBatchEdit: typeof import('./src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue')['default']
+    StdDataDisplayStdCurd: typeof import('./src/components/StdDesign/StdDataDisplay/StdCurd.vue')['default']
+    StdDataDisplayStdPagination: typeof import('./src/components/StdDesign/StdDataDisplay/StdPagination.vue')['default']
+    StdDataDisplayStdTable: typeof import('./src/components/StdDesign/StdDataDisplay/StdTable.vue')['default']
+    StdDataEntryComponentsStdPassword: typeof import('./src/components/StdDesign/StdDataEntry/components/StdPassword.vue')['default']
+    StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelect.vue')['default']
+    StdDataEntryComponentsStdSelector: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelector.vue')['default']
+    StdDataEntryStdFormItem: typeof import('./src/components/StdDesign/StdDataEntry/StdFormItem.vue')['default']
+    StdDesignStdDataDisplayStdBatchEdit: typeof import('./src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue')['default']
+    StdDesignStdDataDisplayStdCurd: typeof import('./src/components/StdDesign/StdDataDisplay/StdCurd.vue')['default']
+    StdDesignStdDataDisplayStdPagination: typeof import('./src/components/StdDesign/StdDataDisplay/StdPagination.vue')['default']
+    StdDesignStdDataDisplayStdTable: typeof import('./src/components/StdDesign/StdDataDisplay/StdTable.vue')['default']
+    StdDesignStdDataEntryComponentsStdPassword: typeof import('./src/components/StdDesign/StdDataEntry/components/StdPassword.vue')['default']
+    StdDesignStdDataEntryComponentsStdSelect: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelect.vue')['default']
+    StdDesignStdDataEntryComponentsStdSelector: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelector.vue')['default']
+    StdDesignStdDataEntryStdFormItem: typeof import('./src/components/StdDesign/StdDataEntry/StdFormItem.vue')['default']
     SwitchAppearanceIconsVPIconMoon: typeof import('./src/components/SwitchAppearance/icons/VPIconMoon.vue')['default']
     SwitchAppearanceIconsVPIconSun: typeof import('./src/components/SwitchAppearance/icons/VPIconSun.vue')['default']
     SwitchAppearanceSwitchAppearance: typeof import('./src/components/SwitchAppearance/SwitchAppearance.vue')['default']

+ 1 - 0
app/gettext.config.js

@@ -1,3 +1,4 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
 const i18n = require('./i18n.json')
 
 module.exports = {

+ 12 - 0
app/package.json

@@ -5,6 +5,7 @@
   "type": "commonjs",
   "scripts": {
     "dev": "vite",
+    "lint": "eslint . -c .eslintrc.js --fix --ext .ts,.vue,.tsx",
     "build": "vite build",
     "preview": "vite preview",
     "gettext:extract": "vue-gettext-extract",
@@ -18,6 +19,7 @@
     "@types/sortablejs": "^1.15.0",
     "@vue/reactivity": "^3.3.9",
     "@vue/shared": "^3.3.9",
+    "@vueuse/core": "^10.6.1",
     "ant-design-vue": "4.0.7",
     "apexcharts": "^3.36.3",
     "axios": "^1.6.2",
@@ -43,11 +45,21 @@
     "xterm-addon-fit": "^0.8.0"
   },
   "devDependencies": {
+    "@antfu/eslint-config-vue": "^0.43.1",
+    "@typescript-eslint/eslint-plugin": "^6.13.0",
+    "@typescript-eslint/parser": "^6.13.0",
     "@vitejs/plugin-vue": "^4.5.0",
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "@vue/compiler-sfc": "^3.3.9",
     "ace-builds": "^1.31.2",
     "autoprefixer": "^10.4.16",
+    "eslint": "^8.54.0",
+    "eslint-import-resolver-alias": "^1.1.2",
+    "eslint-import-resolver-typescript": "^3.6.1",
+    "eslint-plugin-import": "^2.29.0",
+    "eslint-plugin-regex": "^1.10.0",
+    "eslint-plugin-sonarjs": "^0.23.0",
+    "eslint-plugin-vue": "^9.18.1",
     "less": "^4.2.0",
     "postcss": "^8.4.31",
     "tailwindcss": "^3.3.5",

Разница между файлами не показана из-за своего большого размера
+ 1221 - 5
app/pnpm-lock.yaml


+ 10 - 9
app/src/App.vue

@@ -1,25 +1,26 @@
 <script setup lang="ts">
+
 // This starter template is using Vue 3 <script setup> SFCs
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
-import {useSettingsStore} from '@/pinia'
-import {computed, provide} from 'vue'
+import { computed, provide } from 'vue'
+import { useSettingsStore } from '@/pinia'
 
 const media = window.matchMedia('(prefers-color-scheme: dark)')
 
-const callback = (media: { matches: any; }) => {
+const callback = () => {
   const settings = useSettingsStore()
   if (settings.preference_theme === 'auto') {
-    if (media.matches) {
+    if (media.matches)
       settings.set_theme('dark')
-    } else {
+    else
       settings.set_theme('light')
-    }
-  } else {
+  }
+  else {
     settings.set_theme(settings.preference_theme)
   }
 }
 
-callback(media)
+callback()
 
 const devicePrefersTheme = computed(() => {
   return media.matches ? 'dark' : 'light'
@@ -31,7 +32,7 @@ media.addEventListener('change', callback)
 </script>
 
 <template>
-  <router-view/>
+  <RouterView />
 </template>
 
 <style lang="less">

+ 1 - 1
app/src/api/analytic.ts

@@ -3,7 +3,7 @@ import http from '@/lib/http'
 const analytic = {
   init() {
     return http.get('/analytic/init')
-  }
+  },
 }
 
 export default analytic

+ 14 - 11
app/src/api/auth.ts

@@ -1,24 +1,27 @@
 import http from '@/lib/http'
-import {useUserStore} from '@/pinia'
+import { useUserStore } from '@/pinia'
 
-const user = useUserStore()
-const {login, logout} = user
+const { login, logout } = useUserStore()
+
+export interface AuthResponse {
+  token: string
+}
 
 const auth = {
   async login(name: string, password: string) {
     return http.post('/login', {
-      name: name,
-      password: password
-    }).then(r => {
+      name,
+      password,
+    }).then((r: AuthResponse) => {
       login(r.token)
     })
   },
-  async casdoorLogin(code: string, state: string) {
+  async casdoor_login(code: string, state: string) {
     await http.post('/casdoor_callback', {
-      code: code,
-      state: state
+      code,
+      state,
     })
-      .then((r) => {
+      .then((r: AuthResponse) => {
         login(r.token)
       })
   },
@@ -26,7 +29,7 @@ const auth = {
     return http.delete('/logout').then(async () => {
       logout()
     })
-  }
+  },
 }
 
 export default auth

+ 22 - 5
app/src/api/auto_cert.ts

@@ -1,13 +1,30 @@
 import http from '@/lib/http'
 
+export interface DNSProvider {
+  name: string
+  code: string
+  configuration: {
+    credentials: {
+      [key: string]: string
+    }
+    additional: {
+      [key: string]: string
+    }
+  }
+  links: {
+    api: string
+    go_client: string
+  }
+}
+
 const auto_cert = {
-  get_dns_providers() {
-    return http.get('/auto_cert/dns/providers')
+  get_dns_providers(): Promise<DNSProvider[]> {
+    return http.get('/certificate/dns_providers')
   },
 
-  get_dns_provider(code: string) {
-    return http.get('/auto_cert/dns/provider/' + code)
-  }
+  get_dns_provider(code: string): Promise<DNSProvider> {
+    return http.get(`/certificate/dns_provider/${code}`)
+  },
 }
 
 export default auto_cert

+ 23 - 1
app/src/api/cert.ts

@@ -1,5 +1,27 @@
+import type { ModelBase } from '@/api/curd'
 import Curd from '@/api/curd'
+import type { DnsCredential } from '@/api/dns_credential'
 
-const cert = new Curd('/cert')
+export interface Cert extends ModelBase {
+  name: string
+  domains: string[]
+  filename: string
+  ssl_certificate_path: string
+  ssl_certificate_key_path: string
+  auto_cert: number
+  challenge_method: string
+  dns_credential_id: number
+  dns_credential?: DnsCredential
+  log: string
+}
+
+export interface CertificateInfo {
+  subject_name: string
+  issuer_name: string
+  not_after: string
+  not_before: string
+}
+
+const cert: Curd<Cert> = new Curd('/cert')
 
 export default cert

+ 10 - 1
app/src/api/config.ts

@@ -1,5 +1,14 @@
 import Curd from '@/api/curd'
+import type { ChatComplicationMessage } from '@/api/openai'
 
-const config = new Curd('/config')
+export interface Config {
+  name: string
+  content: string
+  chatgpt_messages: ChatComplicationMessage[]
+  file_path: string
+  modified_at: string
+}
+
+const config: Curd<Config> = new Curd('/config')
 
 export default config

+ 31 - 9
app/src/api/curd.ts

@@ -1,6 +1,24 @@
 import http from '@/lib/http'
 
-class Curd {
+export interface ModelBase {
+  id: number
+  created_at: string
+  updated_at: string
+}
+
+export interface Pagination {
+  total: number
+  per_page: number
+  current_page: number
+  total_pages: number
+}
+
+export interface IGetListResponse<T> {
+  data: T[]
+  pagination: Pagination
+}
+
+class Curd<T> {
   protected readonly baseUrl: string
   protected readonly plural: string
 
@@ -11,23 +29,27 @@ class Curd {
 
   constructor(baseUrl: string, plural: string | null = null) {
     this.baseUrl = baseUrl
-    this.plural = plural ?? this.baseUrl + 's'
+    this.plural = plural ?? `${this.baseUrl}s`
   }
 
-  _get_list(params: any = null) {
-    return http.get(this.plural, {params: params})
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  _get_list(params: any = null): Promise<IGetListResponse<T>> {
+    return http.get(this.plural, { params })
   }
 
-  _get(id: any = null) {
-    return http.get(this.baseUrl + (id ? '/' + id : ''))
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  _get(id: any = null): Promise<T> {
+    return http.get(this.baseUrl + (id ? `/${id}` : ''))
   }
 
-  _save(id: any = null, data: any, config: any = undefined) {
-    return http.post(this.baseUrl + (id ? '/' + id : ''), data, config)
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  _save(id: any = null, data: any, config: any = undefined): Promise<T> {
+    return http.post(this.baseUrl + (id ? `/${id}` : ''), data, config)
   }
 
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   _destroy(id: any = null) {
-    return http.delete(this.baseUrl + '/' + id)
+    return http.delete(`${this.baseUrl}/${id}`)
   }
 }
 

+ 9 - 1
app/src/api/dns_credential.ts

@@ -1,5 +1,13 @@
+import type { ModelBase } from '@/api/curd'
 import Curd from '@/api/curd'
+import type { DNSProvider } from '@/api/auto_cert'
 
-const dns_credential = new Curd('/dns_credential')
+export interface DnsCredential extends ModelBase {
+  name: string
+  config?: DNSProvider
+  provider: string
+}
+
+const dns_credential: Curd<DnsCredential> = new Curd('/dns_credential')
 
 export default dns_credential

+ 35 - 12
app/src/api/domain.ts

@@ -1,34 +1,57 @@
 import Curd from '@/api/curd'
 import http from '@/lib/http'
-import {AxiosRequestConfig} from 'axios/index'
+import type { ChatComplicationMessage } from '@/api/openai'
+import type { CertificateInfo } from '@/api/cert'
+import type { NgxConfig } from '@/api/ngx'
+
+export interface Site {
+  modified_at: string
+  advanced: boolean
+  enabled: boolean
+  name: string
+  config: string
+  auto_cert: boolean
+  chatgpt_messages: ChatComplicationMessage[]
+  tokenized?: NgxConfig
+  cert_info?: {
+    [key: number]: CertificateInfo
+  }
+}
+
+export interface AutoCertRequest {
+  dns_credential_id: number
+  challenge_method: string
+  domains: string[]
+}
 
-class Domain extends Curd {
-  enable(name: string, config: AxiosRequestConfig) {
-    return http.post(this.baseUrl + '/' + name + '/enable', undefined, config)
+class Domain extends Curd<Site> {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  enable(name: string, config?: any) {
+    return http.post(`${this.baseUrl}/${name}/enable`, undefined, config)
   }
 
   disable(name: string) {
-    return http.post(this.baseUrl + '/' + name + '/disable')
+    return http.post(`${this.baseUrl}/${name}/disable`)
   }
 
   get_template() {
     return http.get('template')
   }
 
-  add_auto_cert(domain: string, data: any) {
-    return http.post('auto_cert/' + domain, data)
+  add_auto_cert(domain: string, data: AutoCertRequest) {
+    return http.post(`auto_cert/${domain}`, data)
   }
 
   remove_auto_cert(domain: string) {
-    return http.delete('auto_cert/' + domain)
+    return http.delete(`auto_cert/${domain}`)
   }
 
-  duplicate(name: string, data: any) {
-    return http.post(this.baseUrl + '/' + name + '/duplicate', data)
+  duplicate(name: string, data: { name: string }): Promise<{ dst: string }> {
+    return http.post(`${this.baseUrl}/${name}/duplicate`, data)
   }
 
-  advance_mode(name: string, data: any) {
-    return http.post(this.baseUrl + '/' + name + '/advance', data)
+  advance_mode(name: string, data: { advanced: boolean }) {
+    return http.post(`${this.baseUrl}/${name}/advance`, data)
   }
 }
 

+ 9 - 2
app/src/api/install.ts

@@ -1,12 +1,19 @@
 import http from '@/lib/http'
 
+export interface InstallRequest {
+  email: string
+  username: string
+  password: string
+  database: string
+}
+
 const install = {
   get_lock() {
     return http.get('/install')
   },
-  install_nginx_ui(data: any) {
+  install_nginx_ui(data: InstallRequest) {
     return http.post('/install', data)
-  }
+  },
 }
 
 export default install

+ 3 - 2
app/src/api/nginx_log.ts

@@ -9,7 +9,8 @@ export interface INginxLogData {
 
 const nginx_log = {
   page(page = 0, data: INginxLogData) {
-    return http.post('/nginx_log?page=' + page, data)
-  }
+    return http.post(`/nginx_log?page=${page}`, data)
+  },
 }
+
 export default nginx_log

+ 37 - 4
app/src/api/ngx.ts

@@ -1,16 +1,49 @@
 import http from '@/lib/http'
 
+export interface NgxConfig {
+  file_name?: string
+  name: string
+  upstreams?: NgxUpstream[]
+  servers: NgxServer[]
+  custom?: string
+}
+
+export interface NgxServer {
+  directives?: NgxDirective[]
+  locations?: NgxLocation[]
+  comments?: string
+}
+
+export interface NgxUpstream {
+  name: string
+  directives: NgxDirective[]
+  comments?: string
+}
+
+export interface NgxDirective {
+  idx?: number
+  directive: string
+  params: string
+  comments?: string
+}
+
+export interface NgxLocation {
+  path: string
+  content: string
+  comments: string
+}
+
 const ngx = {
-  build_config(ngxConfig: any) {
+  build_config(ngxConfig: NgxConfig) {
     return http.post('/ngx/build_config', ngxConfig)
   },
 
   tokenize_config(content: string) {
-    return http.post('/ngx/tokenize_config', {content})
+    return http.post('/ngx/tokenize_config', { content })
   },
 
   format_code(content: string) {
-    return http.post('/ngx/format_code', {content})
+    return http.post('/ngx/format_code', { content })
   },
 
   status() {
@@ -27,7 +60,7 @@ const ngx = {
 
   test() {
     return http.post('/nginx/test')
-  }
+  },
 }
 
 export default ngx

+ 8 - 2
app/src/api/openai.ts

@@ -1,9 +1,15 @@
 import http from '@/lib/http'
 
+export interface ChatComplicationMessage {
+  role: string
+  content: string
+  name?: string
+}
+
 const openai = {
-  store_record(data: any) {
+  store_record(data: { file_name: string; messages: ChatComplicationMessage[] }) {
     return http.post('/chat_gpt_record', data)
-  }
+  },
 }
 
 export default openai

+ 2 - 1
app/src/api/settings.ts

@@ -4,9 +4,10 @@ const settings = {
   get() {
     return http.get('/settings')
   },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   save(data: any) {
     return http.post('/settings', data)
-  }
+  },
 }
 
 export default settings

+ 25 - 7
app/src/api/template.ts

@@ -1,7 +1,26 @@
 import Curd from '@/api/curd'
 import http from '@/lib/http'
+import type { NgxServer } from '@/api/ngx'
 
-class Template extends Curd {
+export interface Variable {
+  type: string
+  name: { [key: string]: string }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  value: any
+}
+
+export interface Template extends NgxServer {
+  name: string
+  description: { [key: string]: string }
+  author: string
+  filename: string
+  variables: {
+    [key: string]: Variable
+  }
+  custom: string
+}
+
+class TemplateApi extends Curd<Template> {
   get_config_list() {
     return http.get('template/configs')
   }
@@ -11,19 +30,18 @@ class Template extends Curd {
   }
 
   get_config(name: string) {
-    return http.get('template/config/' + name)
+    return http.get(`template/config/${name}`)
   }
 
   get_block(name: string) {
-    return http.get('template/block/' + name)
+    return http.get(`template/block/${name}`)
   }
 
-  build_block(name: string, data: any) {
-    return http.post('template/block/' + name, data)
+  build_block(name: string, data: Variable) {
+    return http.post(`template/block/${name}`, data)
   }
-
 }
 
-const template = new Template('/template')
+const template = new TemplateApi('/template')
 
 export default template

+ 3 - 3
app/src/api/upgrade.ts

@@ -4,13 +4,13 @@ const upgrade = {
   get_latest_release(channel: string) {
     return http.get('/upgrade/release', {
       params: {
-        channel
-      }
+        channel,
+      },
     })
   },
   current_version() {
     return http.get('/upgrade/current')
-  }
+  },
 }
 
 export default upgrade

+ 17 - 15
app/src/components/Breadcrumb/Breadcrumb.vue

@@ -1,9 +1,8 @@
 <script setup lang="ts">
-import {computed, ref} from 'vue'
-import {useRoute} from 'vue-router'
+import { useRoute } from 'vue-router'
 
 interface bread {
-  name: any
+  name: () => string
   path: string
 }
 
@@ -11,35 +10,38 @@ const name = ref()
 const route = useRoute()
 
 const breadList = computed(() => {
-  let _breadList: bread[] = []
+  const _breadList: bread[] = []
 
   name.value = route.name
 
   route.matched.forEach(item => {
-    //item.name !== 'index' && this.breadList.push(item)
+    // item.name !== 'index' && this.breadList.push(item)
     _breadList.push({
-      name: item.name,
-      path: item.path
+      name: item.name as () => string,
+      path: item.path,
     })
   })
 
   return _breadList
 })
 
-
 </script>
 
 <template>
-  <a-breadcrumb class="breadcrumb">
-    <a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
-      <router-link
+  <ABreadcrumb class="breadcrumb">
+    <ABreadcrumbItem
+      v-for="(item, index) in breadList"
+      :key="item.name"
+    >
+      <RouterLink
         v-if="item.name !== name && index !== 1"
         :to="{ path: item.path === '' ? '/' : item.path }"
-      >{{ item.name() }}
-      </router-link>
+      >
+        {{ item.name() }}
+      </RouterLink>
       <span v-else>{{ item.name() }}</span>
-    </a-breadcrumb-item>
-  </a-breadcrumb>
+    </ABreadcrumbItem>
+  </ABreadcrumb>
 </template>
 
 <style scoped>

+ 57 - 46
app/src/components/Chart/AreaChart.vue

@@ -1,86 +1,90 @@
 <script setup lang="ts">
 import VueApexCharts from 'vue3-apexcharts'
-import {ref, watch} from 'vue'
-import {useSettingsStore} from '@/pinia'
-import {storeToRefs} from 'pinia'
+import { storeToRefs } from 'pinia'
+import type { Ref } from 'vue'
+import { useSettingsStore } from '@/pinia'
+import type { Series } from '@/components/Chart/types'
 
-const {series, max, y_formatter} = defineProps(['series', 'max', 'y_formatter'])
+const { series, max, y_formatter } = defineProps<{
+  series: Series[]
+  max?: number
+  yFormatter?: (value: number) => string
+}>()
 
 const settings = useSettingsStore()
-const {theme} = storeToRefs(settings)
+const { theme } = storeToRefs(settings)
 
 const fontColor = () => {
   return theme.value === 'dark' ? '#b4b4b4' : undefined
 }
 
-const chart = ref(null)
+const chart: Ref<ApexCharts | undefined> = ref()
 
 let chartOptions = {
   chart: {
     type: 'area',
     zoom: {
-      enabled: false
+      enabled: false,
     },
     animations: {
-      enabled: false
+      enabled: false,
     },
     toolbar: {
-      show: false
-    }
+      show: false,
+    },
   },
   colors: ['#ff6385', '#36a3eb'],
   fill: {
     // type: ['solid', 'gradient'],
     gradient: {
-      shade: 'light'
-    }
-    //colors:  ['#ff6385', '#36a3eb'],
+      shade: 'light',
+    },
+
+    // colors:  ['#ff6385', '#36a3eb'],
   },
   dataLabels: {
-    enabled: false
+    enabled: false,
   },
   stroke: {
     curve: 'smooth',
-    width: 0
+    width: 0,
   },
   xaxis: {
     type: 'datetime',
     labels: {
       datetimeUTC: false,
       style: {
-        colors: fontColor()
-      }
-    }
+        colors: fontColor(),
+      },
+    },
   },
   tooltip: {
-    enabled: false
+    enabled: false,
   },
   yaxis: {
-    max: max,
+    max,
     tickAmount: 4,
     min: 0,
     labels: {
       style: {
-        colors: fontColor()
+        colors: fontColor(),
       },
-      formatter: y_formatter
-    }
+      formatter: y_formatter,
+    },
   },
   legend: {
     labels: {
-      colors: fontColor()
+      colors: fontColor(),
     },
     onItemClick: {
-      toggleDataSeries: false
+      toggleDataSeries: false,
     },
     onItemHover: {
-      highlightDataSeries: false
-    }
-  }
+      highlightDataSeries: false,
+    },
+  },
 }
 
-let instance: ApexCharts | null = chart.value
-
 const callback = () => {
   chartOptions = {
     ...chartOptions,
@@ -90,45 +94,52 @@ const callback = () => {
         labels: {
           datetimeUTC: false,
           style: {
-            colors: fontColor()
-          }
-        }
+            colors: fontColor(),
+          },
+        },
       },
       yaxis: {
-        max: max,
+        max,
         tickAmount: 4,
         min: 0,
         labels: {
           style: {
-            colors: fontColor()
+            colors: fontColor(),
           },
-          formatter: y_formatter
-        }
+          formatter: y_formatter,
+        },
       },
       legend: {
         labels: {
-          colors: fontColor()
+          colors: fontColor(),
         },
         onItemClick: {
-          toggleDataSeries: false
+          toggleDataSeries: false,
         },
         onItemHover: {
-          highlightDataSeries: false
-        }
-      }
-    }
+          highlightDataSeries: false,
+        },
+      },
+    },
   }
-  instance?.updateOptions?.(chartOptions)
+  chart.value?.updateOptions?.(chartOptions)
 }
+
 watch(theme, callback)
 </script>
 
 <template>
   <!-- Use theme as key to rerender the chart when theme changes to prevent style issues -->
-  <VueApexCharts :key="theme" type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+  <VueApexCharts
+    :key="theme"
+    ref="chart"
+    type="area"
+    height="200"
+    :options="chartOptions"
+    :series="series"
+  />
 </template>
 
-
 <style scoped>
 
 </style>

+ 45 - 27
app/src/components/Chart/RadialBarChart.vue

@@ -1,21 +1,28 @@
 <script setup lang="ts">
 import VueApexCharts from 'vue3-apexcharts'
-import {reactive} from 'vue'
-import {useSettingsStore} from '@/pinia'
-import {storeToRefs} from 'pinia'
+import { reactive } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useSettingsStore } from '@/pinia'
+import type { Series } from '@/components/Chart/types'
 
-const {series, centerText, colors, name, bottomText}
-  = defineProps(['series', 'centerText', 'colors', 'name', 'bottomText'])
+const { series, centerText, colors, name, bottomText }
+  = defineProps<{
+    series: Series[]
+    centerText?: string
+    colors?: string
+    name?: string
+    bottomText?: string
+  }>()
 
 const settings = useSettingsStore()
 
-const {theme} = storeToRefs(settings)
+const { theme } = storeToRefs(settings)
 
 const chartOptions = reactive({
-  series: series,
+  series,
   chart: {
     type: 'radialBar',
-    offsetY: 0
+    offsetY: 0,
   },
   plotOptions: {
     radialBar: {
@@ -25,7 +32,7 @@ const chartOptions = reactive({
         name: {
           fontSize: '14px',
           color: colors,
-          offsetY: 36
+          offsetY: 36,
         },
         value: {
           offsetY: 50,
@@ -33,42 +40,53 @@ const chartOptions = reactive({
           color: undefined,
           formatter: () => {
             return ''
-          }
-        }
-      }
-    }
+          },
+        },
+      },
+    },
   },
   fill: {
-    colors: colors
+    colors,
   },
   labels: [name],
   states: {
     hover: {
       filter: {
-        type: 'none'
-      }
+        type: 'none',
+      },
     },
     active: {
       filter: {
-        type: 'none'
-      }
-    }
-  }
+        type: 'none',
+      },
+    },
+  },
 })
 </script>
 
 <template>
   <!-- Use theme as key to rerender the chart when theme changes to prevent style issues -->
-  <div class="radial-bar-container" :key="theme">
-    <p class="text">{{ centerText }}</p>
-    <p class="bottom_text">{{ bottomText }}</p>
-    <VueApexCharts v-if="centerText" class="radialBar" type="radialBar" height="205" :options="chartOptions"
-                   :series="series"
-                   ref="chart"/>
+  <div
+    :key="theme"
+    class="radial-bar-container"
+  >
+    <p class="text">
+      {{ centerText }}
+    </p>
+    <p class="bottom_text">
+      {{ bottomText }}
+    </p>
+    <VueApexCharts
+      v-if="centerText"
+      class="radialBar"
+      type="radialBar"
+      height="205"
+      :options="chartOptions"
+      :series="series"
+    />
   </div>
 </template>
 
-
 <style lang="less" scoped>
 .radial-bar-container {
   position: relative;

+ 15 - 12
app/src/components/Chart/UsageProgressLine.vue

@@ -1,37 +1,40 @@
 <script setup lang="ts">
-import {computed} from 'vue'
+import { computed } from 'vue'
 
 const props = withDefaults(defineProps<{
   percent: number
 }>(), {
-  percent: 0
+  percent: 0,
 })
 
 const color = computed(() => {
-  if (props.percent < 80) {
+  if (props.percent < 80)
     return '#1890ff'
-  } else if (props.percent >= 80 && props.percent < 90) {
+  else if (props.percent >= 80 && props.percent < 90)
     return '#faad14'
-  } else {
+  else
     return '#ff6385'
-  }
 })
 
 const fixed_percent = computed(() => {
-  return parseFloat(props.percent.toFixed(2))
+  return Number.parseFloat(props.percent.toFixed(2))
 })
 </script>
 
 <template>
   <div>
     <div>
-      <span class="slot-icon"><slot name="icon"></slot></span>
+      <span class="slot-icon"><slot name="icon" /></span>
       <span class="slot">
-                <slot></slot>
-            </span>
-      <span class="dot"> ·</span> {{ fixed_percent + '%' }}
+        <slot />
+      </span>
+      <span class="dot"> ·</span> {{ `${fixed_percent}%` }}
     </div>
-    <a-progress :percent="fixed_percent" :stroke-color="color" :show-info="false"/>
+    <AProgress
+      :percent="fixed_percent"
+      :stroke-color="color"
+      :show-info="false"
+    />
   </div>
 </template>
 

+ 4 - 0
app/src/components/Chart/types.d.ts

@@ -0,0 +1,4 @@
+export interface Series {
+  name: string
+  data: []
+}

+ 154 - 97
app/src/components/ChatGPT/ChatGPT.vue

@@ -1,47 +1,56 @@
 <script setup lang="ts">
-import {computed, onMounted, ref, watch} from 'vue'
-import {useGettext} from 'vue3-gettext'
-import {useUserStore} from '@/pinia'
-import {storeToRefs} from 'pinia'
-import {urlJoin} from '@/lib/helper'
-import {marked} from 'marked'
+import Icon, { SendOutlined } from '@ant-design/icons-vue'
+import { useGettext } from 'vue3-gettext'
+import { storeToRefs } from 'pinia'
+import { marked } from 'marked'
 import hljs from 'highlight.js'
+import type { Ref } from 'vue'
+import { urlJoin } from '@/lib/helper'
+import { useSettingsStore, useUserStore } from '@/pinia'
 import 'highlight.js/styles/vs2015.css'
-import Icon, {SendOutlined} from '@ant-design/icons-vue'
 
+import type { ChatComplicationMessage } from '@/api/openai'
 import openai from '@/api/openai'
 import ChatGPT_logo from '@/assets/svg/ChatGPT_logo.svg'
 
-const {$gettext} = useGettext()
+const props = defineProps<{
+  content: string
+  path?: string
+  historyMessages: ChatComplicationMessage[]
+}>()
 
-const props = defineProps(['content', 'path', 'history_messages'])
 const emit = defineEmits(['update:history_messages'])
-const history_messages = computed(() => props.history_messages)
+
+const { $gettext } = useGettext()
+
+const { language: current } = storeToRefs(useSettingsStore())
+
+const history_messages = computed(() => props.historyMessages)
+const messages = ref([]) as Ref<ChatComplicationMessage[]>
 
 onMounted(() => {
-  messages.value = props.history_messages
+  messages.value = props.historyMessages
 })
 
 watch(history_messages, () => {
-  messages.value = props.history_messages
+  messages.value = props.historyMessages
 })
 
-const {current} = useGettext()
-
-const messages: any = ref([])
-
 const loading = ref(false)
 const ask_buffer = ref('')
 
+// eslint-disable-next-line sonarjs/cognitive-complexity
 async function request() {
   loading.value = true
+
   const t = ref({
     role: 'assistant',
-    content: ''
+    content: '',
   })
+
   const user = useUserStore()
 
-  const {token} = storeToRefs(user)
+  const { token } = storeToRefs(user)
 
   console.log('fetching...')
 
@@ -49,35 +58,39 @@ async function request() {
 
   emit('update:history_messages', messages.value)
 
-  let res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
+  const res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
     method: 'POST',
-    headers: {'Accept': 'text/event-stream', Authorization: token.value},
-    body: JSON.stringify({messages: messages.value.slice(0, messages.value?.length - 1)})
+    headers: { Accept: 'text/event-stream', Authorization: token.value },
+    body: JSON.stringify({ messages: messages.value.slice(0, messages.value?.length - 1) }),
   })
+
   // read body as stream
-  console.log('reading...')
-  let reader = res.body!.getReader()
+  console.info('reading...')
+
+  const reader = res.body!.getReader()
 
   // read stream
-  console.log('reading stream...')
+  console.info('reading stream...')
 
   let buffer = ''
 
   let hasCodeBlockIndicator = false
 
   while (true) {
-    let {done, value} = await reader.read()
+    const { done, value } = await reader.read()
     if (done) {
-      console.log('done')
+      console.info('done')
+      setTimeout(() => {
+        scrollToBottom()
+      }, 500)
       loading.value = false
       store_record()
       break
     }
-
-    apply(value)
+    apply(value!)
   }
 
-  function apply(input: any) {
+  function apply(input: Uint8Array) {
     const decoder = new TextDecoder('utf-8')
     const raw = decoder.decode(input)
 
@@ -87,56 +100,66 @@ async function request() {
 
     line?.forEach(v => {
       const data = v.slice('event:message\ndata:'.length)
-      if (!data) {
+      if (!data)
         return
-      }
+
       const content = JSON.parse(data).content
 
-      if (!hasCodeBlockIndicator) {
-        hasCodeBlockIndicator = content.indexOf('`') > -1
-      }
+      if (!hasCodeBlockIndicator)
+        hasCodeBlockIndicator = content.includes('`')
 
-      for (let c of content) {
+      for (const c of content) {
         buffer += c
         if (hasCodeBlockIndicator) {
           if (isCodeBlockComplete(buffer)) {
             t.value.content = buffer
             hasCodeBlockIndicator = false
-          } else {
-            t.value.content = buffer + '\n```'
           }
-        } else {
+          else {
+            t.value.content = `${buffer}\n\`\`\``
+          }
+        }
+        else {
           t.value.content = buffer
         }
       }
+
+      // keep container scroll to bottom
+      scrollToBottom()
     })
   }
 
   function isCodeBlockComplete(text: string) {
     const codeBlockRegex = /```/g
     const matches = text.match(codeBlockRegex)
-    if (matches) {
+    if (matches)
       return matches.length % 2 === 0
-    } else {
+    else
       return true
-    }
   }
 
+  function scrollToBottom() {
+    const container = document.querySelector('.right-settings .ant-card-body')
+    if (container)
+      container.scrollTop = container.scrollHeight
+  }
 }
 
 async function send() {
-  if (!messages.value) {
+  if (!messages.value)
     messages.value = []
-  }
+
   if (messages.value.length === 0) {
+    console.log(current.value)
     messages.value.push({
       role: 'user',
-      content: props.content + '\n\nCurrent Language Code: ' + current
+      content: `${props.content}\n\nCurrent Language Code: ${current.value}`,
     })
-  } else {
+  }
+  else {
     messages.value.push({
       role: 'user',
-      content: ask_buffer.value
+      content: ask_buffer.value,
     })
     ask_buffer.value = ''
   }
@@ -144,109 +167,143 @@ async function send() {
 }
 
 const renderer = new marked.Renderer()
+
 renderer.code = (code, lang: string) => {
   const language = hljs.getLanguage(lang) ? lang : 'nginx'
-  const highlightedCode = hljs.highlight(code, {language}).value
+  const highlightedCode = hljs.highlight(code, { language }).value
+
   return `<pre><code class="hljs ${language}">${highlightedCode}</code></pre>`
 }
 
 marked.setOptions({
-  renderer: renderer,
-  langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
+  renderer,
   pedantic: false,
   gfm: true,
   breaks: false,
-  sanitize: false,
-  smartypants: true,
-  xhtml: false
 })
 
 function store_record() {
   openai.store_record({
     file_name: props.path,
-    messages: messages.value
+    messages: messages.value,
   })
 }
 
 function clear_record() {
   openai.store_record({
     file_name: props.path,
-    messages: []
+    messages: [],
   })
   messages.value = []
   emit('update:history_messages', [])
 }
-
+const editing_idx = ref(-1)
 async function regenerate(index: number) {
   editing_idx.value = -1
   messages.value = messages.value.slice(0, index)
   await request()
 }
 
-const editing_idx = ref(-1)
-
-const show = computed(() => messages?.value?.length === 0)
-
+const show = computed(() => !messages.value || messages.value?.length === 0)
 </script>
 
 <template>
-  <div class="chat-start" v-if="show">
-    <a-button @click="send" :loading="loading">
-      <Icon v-if="!loading" :component="ChatGPT_logo"/>
+  <div
+    v-if="show"
+    class="chat-start"
+  >
+    <AButton
+      :loading="loading"
+      @click="send"
+    >
+      <Icon
+        v-if="!loading"
+        :component="ChatGPT_logo"
+      />
       {{ $gettext('Ask ChatGPT for Help') }}
-    </a-button>
+    </AButton>
   </div>
-  <div class="chatgpt-container" v-else>
-    <a-list
+  <div
+    v-else
+    class="chatgpt-container"
+  >
+    <AList
       class="chatgpt-log"
       item-layout="horizontal"
       :data-source="messages"
     >
       <template #renderItem="{ item, index }">
-        <a-list-item>
-          <a-comment :author="item.role==='assistant'?$gettext('Assistant'):$gettext('User')">
+        <AListItem>
+          <AComment :author="item.role === 'assistant' ? $gettext('Assistant') : $gettext('User')">
             <template #content>
-              <div class="content" v-if="item.role==='assistant'||editing_idx!=index"
-                   v-html="marked.parse(item.content)"></div>
-              <a-input style="padding: 0" v-else v-model:value="item.content"
-                       :bordered="false"/>
+              <div
+                v-if="item.role === 'assistant' || editing_idx !== index"
+                class="content"
+                v-html="marked.parse(item.content)"
+              />
+              <AInput
+                v-else
+                v-model:value="item.content"
+                style="padding: 0"
+                :bordered="false"
+              />
             </template>
             <template #actions>
-                                    <span v-if="item.role==='user'&&editing_idx!==index" @click="editing_idx=index">
-                                        {{ $gettext('Modify') }}
-                                    </span>
-              <template v-else-if="editing_idx==index">
-                <span @click="regenerate(index+1)">{{ $gettext('Save') }}</span>
-                <span @click="editing_idx=-1">{{ $gettext('Cancel') }}</span>
+              <span
+                v-if="item.role === 'user' && editing_idx !== index"
+                @click="editing_idx = index"
+              >
+                {{ $gettext('Modify') }}
+              </span>
+              <template v-else-if="editing_idx === index">
+                <span @click="regenerate(index + 1)">{{ $gettext('Save') }}</span>
+                <span @click="editing_idx = -1">{{ $gettext('Cancel') }}</span>
               </template>
-              <span v-else-if="!loading" @click="regenerate(index)" :disabled="loading">
-                                        {{ $gettext('Reload') }}
-                                    </span>
+              <span
+                v-else-if="!loading"
+                @click="regenerate(index)"
+              >
+                {{ $gettext('Reload') }}
+              </span>
             </template>
-          </a-comment>
-        </a-list-item>
+          </AComment>
+        </AListItem>
       </template>
-    </a-list>
+    </AList>
     <div class="input-msg">
       <div class="control-btn">
-        <a-space v-show="!loading">
-          <a-popconfirm
-            :cancelText="$gettext('No')"
-            :okText="$gettext('OK')"
+        <ASpace v-show="!loading">
+          <APopconfirm
+            :cancel-text="$gettext('No')"
+            :ok-text="$gettext('OK')"
             :title="$gettext('Are you sure you want to clear the record of chat?')"
-            @confirm="clear_record">
-            <a-button type="text">{{ $gettext('Clear') }}</a-button>
-          </a-popconfirm>
-          <a-button type="text" @click="regenerate(messages?.length-1)">
+            @confirm="clear_record"
+          >
+            <AButton type="text">
+              {{ $gettext('Clear') }}
+            </AButton>
+          </APopconfirm>
+          <AButton
+            type="text"
+            @click="regenerate(messages?.length - 1)"
+          >
             {{ $gettext('Regenerate response') }}
-          </a-button>
-        </a-space>
+          </AButton>
+        </ASpace>
       </div>
-      <a-textarea auto-size v-model:value="ask_buffer"/>
+      <ATextarea
+        v-model:value="ask_buffer"
+        auto-size
+      />
       <div class="sned-btn">
-        <a-button size="small" type="text" :loading="loading" @click="send">
-          <send-outlined/>
-        </a-button>
+        <AButton
+          size="small"
+          type="text"
+          :loading="loading"
+          @click="send"
+        >
+          <SendOutlined />
+        </AButton>
       </div>
     </div>
   </div>

+ 14 - 10
app/src/components/CodeEditor/CodeEditor.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
-import {VAceEditor} from 'vue3-ace-editor'
+import { VAceEditor } from 'vue3-ace-editor'
 import 'ace-builds/src-noconflict/mode-nginx'
 import 'ace-builds/src-noconflict/theme-monokai'
-import {computed} from 'vue'
+import { computed } from 'vue'
 
-const props = defineProps(['content', 'defaultHeight'])
+const props = defineProps<{
+  content?: string
+  defaultHeight?: string
+}>()
 
 const emit = defineEmits(['update:content'])
 
@@ -12,21 +15,22 @@ const value = computed({
   get() {
     return props.content ?? ''
   },
-  set(value) {
-    emit('update:content', value)
-  }
+  set(v) {
+    emit('update:content', v)
+  },
 })
 </script>
 
 <template>
-  <v-ace-editor
+  <VAceEditor
     v-model:value="value"
     lang="nginx"
     theme="monokai"
     :style="{
-            minHeight: defaultHeight || '100vh',
-            borderRadius: '5px'
-        }"/>
+      minHeight: defaultHeight || '100vh',
+      borderRadius: '5px',
+    }"
+  />
 </template>
 
 <style scoped>

+ 1 - 1
app/src/components/CodeEditor/index.ts

@@ -1,3 +1,3 @@
-import CodeEditor from './CodeEditor'
+import CodeEditor from './CodeEditor.vue'
 
 export default CodeEditor

+ 25 - 19
app/src/components/EnvIndicator/EnvIndicator.vue

@@ -1,15 +1,15 @@
 <script setup lang="ts">
-import {useGettext} from 'vue3-gettext'
-import {CloseOutlined, DashboardOutlined, DatabaseOutlined} from '@ant-design/icons-vue'
-import {useSettingsStore} from '@/pinia'
-import {storeToRefs} from 'pinia'
-import {useRouter} from 'vue-router'
-import {computed, watch} from 'vue'
+import { useGettext } from 'vue3-gettext'
+import { CloseOutlined, DashboardOutlined, DatabaseOutlined } from '@ant-design/icons-vue'
+import { storeToRefs } from 'pinia'
+import { useRouter } from 'vue-router'
+import { computed, watch } from 'vue'
+import { useSettingsStore } from '@/pinia'
 
-const {$gettext} = useGettext()
+const { $gettext } = useGettext()
 const settingsStore = useSettingsStore()
 
-const {environment} = storeToRefs(settingsStore)
+const { environment } = storeToRefs(settingsStore)
 const router = useRouter()
 
 async function clear_env() {
@@ -32,17 +32,23 @@ watch(node_id, async () => {
 <template>
   <div class="indicator">
     <div class="container">
-      <database-outlined/>
-      <span class="env-name" v-if="is_local">
-                 {{ $gettext('Local') }}
-            </span>
-      <span class="env-name" v-else>
-                 {{ environment.name }}
-            </span>
-      <a-tag @click="clear_env">
-        <dashboard-outlined v-if="is_local"/>
-        <close-outlined v-else/>
-      </a-tag>
+      <DatabaseOutlined />
+      <span
+        v-if="is_local"
+        class="env-name"
+      >
+        {{ $gettext('Local') }}
+      </span>
+      <span
+        v-else
+        class="env-name"
+      >
+        {{ environment.name }}
+      </span>
+      <ATag @click="clear_env">
+        <DashboardOutlined v-if="is_local" />
+        <CloseOutlined v-else />
+      </ATag>
     </div>
   </div>
 </template>

+ 7 - 5
app/src/components/FooterToolbar/FooterToolBar.vue

@@ -6,16 +6,18 @@ defineProps<{
 </script>
 
 <template>
-  <teleport to="body">
-    <div class="ant-pro-footer-toolbar" ref="refToolBar">
+  <Teleport to="body">
+    <div class="ant-pro-footer-toolbar">
       <div style="float: left">
-        <slot name="extra">{{ extra }}</slot>
+        <slot name="extra">
+          {{ extra }}
+        </slot>
       </div>
       <div style="float: right">
-        <slot></slot>
+        <slot />
       </div>
     </div>
-  </teleport>
+  </Teleport>
 </template>
 
 <style lang="less" scoped>

+ 7 - 2
app/src/components/Logo/Logo.vue

@@ -3,8 +3,13 @@ import logo from '@/assets/img/logo.png'</script>
 
 <template>
   <div class="logo">
-    <img :src="logo" alt="logo"/>
-    <p class="text">Nginx UI</p>
+    <img
+      :src="logo"
+      alt="logo"
+    >
+    <p class="text">
+      Nginx UI
+    </p>
   </div>
 </template>
 

+ 58 - 36
app/src/components/NginxControl/NginxControl.vue

@@ -1,36 +1,32 @@
 <script setup lang="ts">
+import { message } from 'ant-design-vue'
+import { ReloadOutlined } from '@ant-design/icons-vue'
 import gettext from '@/gettext'
 import ngx from '@/api/ngx'
-import logLevel from '@/views/config/constants'
-import {message} from 'ant-design-vue'
-import {ReloadOutlined} from '@ant-design/icons-vue'
-
-import {ref, watch} from 'vue'
-
-const {$gettext} = gettext
+import { logLevel } from '@/views/config/constants'
 
+const { $gettext } = gettext
+const status = ref(0)
 function get_status() {
   ngx.status().then(r => {
-    if (r?.running === true) {
+    if (r?.running === true)
       status.value = 0
-    } else {
+    else
       status.value = -1
-    }
   })
 }
 
 function reload_nginx() {
   status.value = 1
   ngx.reload().then(r => {
-    if (r.level < logLevel.Warn) {
+    if (r.level < logLevel.Warn)
       message.success($gettext('Nginx reloaded successfully'))
-    } else if (r.level === logLevel.Warn) {
+    else if (r.level === logLevel.Warn)
       message.warn(r.message)
-    } else {
+    else
       message.error(r.message)
-    }
   }).catch(e => {
-    message.error($gettext('Server error') + ' ' + e?.message)
+    message.error(`${$gettext('Server error')} ${e?.message}`)
   }).finally(() => {
     status.value = 0
   })
@@ -39,52 +35,78 @@ function reload_nginx() {
 function restart_nginx() {
   status.value = 2
   ngx.restart().then(r => {
-    if (r.level < logLevel.Warn) {
+    if (r.level < logLevel.Warn)
       message.success($gettext('Nginx restarted successfully'))
-    } else if (r.level === logLevel.Warn) {
+    else if (r.level === logLevel.Warn)
       message.warn(r.message)
-    } else {
+    else
       message.error(r.message)
-    }
   }).catch(e => {
-    message.error($gettext('Server error') + ' ' + e?.message)
+    message.error(`${$gettext('Server error')} ${e?.message}`)
   }).finally(() => {
     status.value = 0
   })
 }
 
-const status = ref(0)
-
 const visible = ref(false)
 
-watch(visible, (v) => {
-  if (v) get_status()
+watch(visible, v => {
+  if (v)
+    get_status()
 })
 </script>
 
 <template>
-  <a-popover
+  <APopover
     v-model:open="visible"
-    @confirm="reload_nginx"
     placement="bottomRight"
+    @confirm="reload_nginx"
   >
     <template #content>
       <div class="content-wrapper">
         <h4>{{ $gettext('Nginx Control') }}</h4>
-        <a-badge v-if="status===0" color="green" :text="$gettext('Running')"/>
-        <a-badge v-else-if="status===1" color="blue" :text="$gettext('Reloading')"/>
-        <a-badge v-else-if="status===2" color="orange" :text="$gettext('Restarting')"/>
-        <a-badge v-else color="red" :text="$gettext('Stopped')"/>
+        <ABadge
+          v-if="status === 0"
+          color="green"
+          :text="$gettext('Running')"
+        />
+        <ABadge
+          v-else-if="status === 1"
+          color="blue"
+          :text="$gettext('Reloading')"
+        />
+        <ABadge
+          v-else-if="status === 2"
+          color="orange"
+          :text="$gettext('Restarting')"
+        />
+        <ABadge
+          v-else
+          color="red"
+          :text="$gettext('Stopped')"
+        />
       </div>
-      <a-space>
-        <a-button size="small" @click="restart_nginx" type="link">{{ $gettext('Restart') }}</a-button>
-        <a-button size="small" @click="reload_nginx" type="link">{{ $gettext('Reload') }}</a-button>
-      </a-space>
+      <ASpace>
+        <AButton
+          size="small"
+          type="link"
+          @click="restart_nginx"
+        >
+          {{ $gettext('Restart') }}
+        </AButton>
+        <AButton
+          size="small"
+          type="link"
+          @click="reload_nginx"
+        >
+          {{ $gettext('Reload') }}
+        </AButton>
+      </ASpace>
     </template>
     <a>
-      <ReloadOutlined/>
+      <ReloadOutlined />
     </a>
-  </a-popover>
+  </APopover>
 </template>
 
 <style lang="less" scoped>

+ 53 - 21
app/src/components/NodeSelector/NodeSelector.vue

@@ -1,12 +1,17 @@
 <script setup lang="ts">
-import {computed, ref} from 'vue'
+import { computed, ref } from 'vue'
+import { useGettext } from 'vue3-gettext'
 import environment from '@/api/environment'
-import {useGettext} from 'vue3-gettext'
 
-const {$gettext} = useGettext()
+const props = defineProps<{
+  target: number[]
+  map?: Record<number, string>
+  hiddenLocal?: boolean
+}>()
 
-const props = defineProps(['target', 'map', 'hidden_local'])
-const emit = defineEmits(['update:target'])
+const emit = defineEmits(['update:target', 'update:map'])
+
+const { $gettext } = useGettext()
 
 const data = ref([])
 const data_map = ref({})
@@ -25,29 +30,56 @@ const value = computed({
   set(v) {
     if (typeof props.map === 'object') {
       v.forEach(id => {
-        if (id !== 0) props.map[id] = data_map[id].name
+        if (id !== 0)
+          emit('update:map', { ...props.map, [id]: data_map[id].name })
       })
     }
     emit('update:target', v)
-  }
+  },
 })
 </script>
 
 <template>
-  <a-checkbox-group v-model:value="value" style="width: 100%">
-    <a-row :gutter="[16,16]">
-      <a-col :span="8" v-if="!hidden_local">
-        <a-checkbox :value="0">{{ $gettext('Local') }}</a-checkbox>
-        <a-tag color="blue">{{ $gettext('Online') }}</a-tag>
-      </a-col>
-      <a-col :span="8" v-for="node in data">
-        <a-checkbox :value="node.id">{{ node.name }}</a-checkbox>
-        <a-tag color="blue" v-if="node.status">{{ $gettext('Online') }}</a-tag>
-        <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
-      </a-col>
-    </a-row>
-    <a-empty v-if="hidden_local&&data.length===0"/>
-  </a-checkbox-group>
+  <ACheckboxGroup
+    v-model:value="value"
+    style="width: 100%"
+  >
+    <ARow :gutter="[16, 16]">
+      <ACol
+        v-if="!hiddenLocal"
+        :span="8"
+      >
+        <ACheckbox :value="0">
+          {{ $gettext('Local') }}
+        </ACheckbox>
+        <ATag color="blue">
+          {{ $gettext('Online') }}
+        </ATag>
+      </ACol>
+      <ACol
+        v-for="(node, index) in data"
+        :key="index"
+        :span="8"
+      >
+        <ACheckbox :value="node.id">
+          {{ node.name }}
+        </ACheckbox>
+        <ATag
+          v-if="node.status"
+          color="blue"
+        >
+          {{ $gettext('Online') }}
+        </ATag>
+        <ATag
+          v-else
+          color="error"
+        >
+          {{ $gettext('Offline') }}
+        </ATag>
+      </ACol>
+    </ARow>
+    <AEmpty v-if="hiddenLocal && data.length === 0" />
+  </ACheckboxGroup>
 </template>
 
 <style scoped lang="less">

+ 11 - 11
app/src/components/PageHeader/PageHeader.vue

@@ -1,9 +1,7 @@
 <script setup lang="ts">
+import { useRoute } from 'vue-router'
+import type { Ref } from 'vue'
 import Breadcrumb from '@/components/Breadcrumb/Breadcrumb.vue'
-import {useRoute} from 'vue-router'
-import {computed, ref, watch} from 'vue'
-
-const {title, logo, avatar} = defineProps(['title', 'logo', 'avatar'])
 
 const route = useRoute()
 
@@ -11,26 +9,28 @@ const display = computed(() => {
   return !route.meta.hiddenHeaderContent
 })
 
-const name = ref(route.name)
+const name = ref(route.name) as Ref<() => string>
+
 watch(() => route.name, () => {
-  name.value = route.name
+  name.value = route.name as () => string
 })
-
 </script>
 
 <template>
-  <div v-if="display" class="page-header">
+  <div
+    v-if="display"
+    class="page-header"
+  >
     <div class="page-header-index-wide">
-      <Breadcrumb/>
+      <Breadcrumb />
       <div class="detail">
         <div class="main">
           <div class="row">
-            <img v-if="logo" :src="logo" class="logo"/>
             <h1 class="title">
               {{ name() }}
             </h1>
             <div class="action">
-              <slot name="action"></slot>
+              <slot name="action" />
             </div>
           </div>
         </div>

+ 20 - 12
app/src/components/SetLanguage/SetLanguage.vue

@@ -1,10 +1,8 @@
 <script setup lang="ts">
+import { ref, watch } from 'vue'
 import gettext from '@/gettext'
 
-import {ref, watch} from 'vue'
-
-import {useSettingsStore} from '@/pinia'
-import {useRoute} from 'vue-router'
+import { useSettingsStore } from '@/pinia'
 import http from '@/lib/http'
 
 const settings = useSettingsStore()
@@ -17,7 +15,7 @@ const languageAvailable = gettext.available
 
 function init() {
   if (current.value !== 'en') {
-    http.get('/translation/' + current.value).then(r => {
+    http.get(`/translation/${current.value}`).then(r => {
       gettext.translations[current.value] = r
     })
   }
@@ -25,23 +23,33 @@ function init() {
 
 init()
 
-watch(current, (v) => {
+watch(current, v => {
   init()
   settings.set_language(v)
   gettext.current = v
-  // @ts-ignored
-  document.title = route.name() + ' | Nginx UI'
+
+  const name = route.name as () => string
+
+  document.title = `${name()} | Nginx UI`
 })
 
 </script>
 
 <template>
   <div>
-    <a-select v-model:value="current" size="small" style="width: 60px">
-      <a-select-option v-for="(language, key) in languageAvailable" :value="key" :key="key">
+    <ASelect
+      v-model:value="current"
+      size="small"
+      style="width: 60px"
+    >
+      <ASelectOption
+        v-for="(language, key) in languageAvailable"
+        :key="key"
+        :value="key"
+      >
         {{ language }}
-      </a-select-option>
-    </a-select>
+      </ASelectOption>
+    </ASelect>
   </div>
 </template>
 

+ 0 - 37
app/src/components/StdDataEntry/StdDataEntry.tsx

@@ -1,37 +0,0 @@
-import {defineComponent} from 'vue'
-import {Form} from 'ant-design-vue'
-import StdFormItem from '@/components/StdDataEntry/StdFormItem.vue'
-import './style.less'
-
-export default defineComponent({
-  props: ['dataList', 'dataSource', 'error', 'layout'],
-  emits: ['update:dataSource'],
-  setup(props, {slots}) {
-    return () => {
-      const template: any = []
-      props.dataList.forEach((v: any) => {
-        let show = true
-        if (v.edit.show) {
-          if (typeof v.edit.show === 'boolean') {
-            show = v.edit.show
-          } else if (typeof v.edit.show === 'function') {
-            show = v.edit.show(props.dataSource)
-          }
-        }
-        if (v.edit.type && show) {
-          template.push(
-            <StdFormItem dataIndex={v.dataIndex} label={v.title()} extra={v.extra} error={props.error}>
-              {v.edit.type(v.edit, props.dataSource, v.dataIndex)}
-            </StdFormItem>
-          )
-        }
-      })
-
-      if (slots.action) {
-        template.push(<div class={'std-data-entry-action'}>{slots.action()}</div>)
-      }
-
-      return <Form layout={props.layout || 'vertical'}>{template}</Form>
-    }
-  }
-})

+ 0 - 45
app/src/components/StdDataEntry/components/StdSelect.vue

@@ -1,45 +0,0 @@
-<script setup lang="ts">
-import {computed, ref} from 'vue'
-import {SelectProps} from 'ant-design-vue'
-
-const props = defineProps(['value', 'mask'])
-const emit = defineEmits(['update:value'])
-
-const options = computed(() => {
-  const _options = ref<SelectProps['options']>([])
-
-  for (const [key, value] of Object.entries(props.mask)) {
-    const v = value as any
-    _options.value!.push({label: v?.(), value: key})
-  }
-
-  return _options
-})
-
-const _value = computed({
-  get() {
-    let v
-
-    if (typeof props.mask?.[props.value] === 'function') {
-      v = props.mask[props.value]()
-    } else if (typeof props.mask?.[props.value] === 'string') {
-      v = props.mask[props.value]
-    } else {
-      v = props.value
-    }
-    return v
-  },
-  set(v) {
-    emit('update:value', v)
-  }
-})
-</script>
-
-<template>
-  <a-select v-model:value="_value"
-            :options="options.value" style="min-width: 180px"/>
-</template>
-
-<style lang="less" scoped>
-
-</style>

+ 0 - 133
app/src/components/StdDataEntry/index.tsx

@@ -1,133 +0,0 @@
-import StdDataEntry from './StdDataEntry.js'
-import {h} from 'vue'
-import {Input, InputNumber, Switch, Textarea} from 'ant-design-vue'
-import StdSelector from './components/StdSelector.vue'
-import StdSelect from './components/StdSelect.vue'
-import StdPassword from './components/StdPassword.vue'
-
-interface IEdit {
-  type: Function
-  placeholder: any
-  mask: any
-  key: any
-  value: any
-  recordValueIndex: any
-  selectionType: any
-  api: Object,
-  columns: any,
-  data_key: any,
-  disable_search: boolean,
-  get_params: Object,
-  description: string
-  generate: boolean
-  min: number
-  max: number,
-  extra: string
-}
-
-function fn(obj: Object, desc: any) {
-  let arr: string[]
-  if (typeof desc === 'string') {
-    arr = desc.split('.')
-  } else {
-    arr = [...desc]
-  }
-
-  while (arr.length) {
-    // @ts-ignore
-    const top = obj[arr.shift()]
-    if (top === undefined) {
-      return null
-    }
-    obj = top
-  }
-  return obj
-}
-
-function readonly(edit: IEdit, dataSource: any, dataIndex: any) {
-  return h('p', fn(dataSource, dataIndex))
-}
-
-function input(edit: IEdit, dataSource: any, dataIndex: any) {
-  return h(Input, {
-    placeholder: edit.placeholder?.() ?? '',
-    value: dataSource?.[dataIndex],
-    'onUpdate:value': value => {
-      dataSource[dataIndex] = value
-    }
-  })
-}
-
-function inputNumber(edit: IEdit, dataSource: any, dataIndex: any) {
-  return h(InputNumber, {
-    placeholder: edit.placeholder?.() ?? '',
-    min: edit.min,
-    max: edit.max,
-    value: dataSource?.[dataIndex],
-    'onUpdate:value': value => {
-      dataSource[dataIndex] = value
-    }
-  })
-}
-
-function textarea(edit: IEdit, dataSource: any, dataIndex: any) {
-  return h(Textarea, {
-    placeholder: edit.placeholder?.() ?? '',
-    value: dataSource?.[dataIndex],
-    'onUpdate:value': value => {
-      dataSource[dataIndex] = value
-    }
-  })
-}
-
-function password(edit: IEdit, dataSource: any, dataIndex: any) {
-  return <StdPassword
-    v-model:value={dataSource[dataIndex]}
-    generate={edit.generate}
-    placeholder={edit.placeholder}
-  />
-}
-
-function select(edit: IEdit, dataSource: any, dataIndex: any) {
-  return <StdSelect
-    v-model:value={dataSource[dataIndex]}
-    mask={edit.mask}
-  />
-}
-
-function selector(edit: IEdit, dataSource: any, dataIndex: any) {
-  return <StdSelector
-    v-model:selectedKey={dataSource[dataIndex]}
-    value={edit.value}
-    recordValueIndex={edit.recordValueIndex}
-    selectionType={edit.selectionType}
-    api={edit.api}
-    columns={edit.columns}
-    data_key={edit.data_key}
-    disable_search={edit.disable_search}
-    get_params={edit.get_params}
-    description={edit.description}
-  />
-}
-
-function antSwitch(edit: IEdit, dataSource: any, dataIndex: any) {
-  return h(Switch, {
-    checked: dataSource?.[dataIndex],
-    'onUpdate:checked': (value: any) => {
-      dataSource[dataIndex] = value
-    }
-  })
-}
-
-export {
-  readonly,
-  input,
-  textarea,
-  select,
-  selector,
-  password,
-  inputNumber,
-  antSwitch
-}
-
-export default StdDataEntry

+ 18 - 17
app/src/components/StdDataDisplay/StdBatchEdit.vue → app/src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue

@@ -1,21 +1,24 @@
 <script setup lang="ts">
-import {reactive, ref} from 'vue'
+import { message } from 'ant-design-vue'
 import gettext from '@/gettext'
-import StdDataEntry from '@/components/StdDataEntry'
-import {message} from 'ant-design-vue'
+import StdDataEntry from '@/components/StdDesign/StdDataEntry'
 
-const {$gettext} = gettext
+const props = defineProps<{
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  api: (ids: number[], data: any) => Promise<void>
+  beforeSave?: () => Promise<void>
+}>()
 
 const emit = defineEmits(['onSave'])
 
-const props = defineProps(['api', 'beforeSave'])
+const { $gettext } = gettext
 
 const batchColumns = ref([])
 
 const visible = ref(false)
 
 const selectedRowKeys = ref([])
-
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 function showModal(c: any, rowKeys: any) {
   visible.value = true
   selectedRowKeys.value = rowKeys
@@ -23,7 +26,7 @@ function showModal(c: any, rowKeys: any) {
 }
 
 defineExpose({
-  showModal
+  showModal,
 })
 
 const data = reactive({})
@@ -38,7 +41,7 @@ async function ok() {
   await props.api(selectedRowKeys.value, data).then(async () => {
     message.success($gettext('Save successfully'))
     emit('onSave')
-  }).catch((e: any) => {
+  }).catch(e => {
     message.error($gettext(e?.message) ?? $gettext('Server error'))
   }).finally(() => {
     loading.value = false
@@ -47,28 +50,26 @@ async function ok() {
 </script>
 
 <template>
-  <a-modal
+  <AModal
+    v-model:open="visible"
     class="std-curd-edit-modal"
     :mask="false"
     :title="$gettext('Batch Modify')"
-    v-model:open="visible"
     :cancel-text="$gettext('Cancel')"
     :ok-text="$gettext('OK')"
-    @ok="ok"
     :confirm-loading="loading"
     :width="600"
-    destroyOnClose
+    destroy-on-close
+    @ok="ok"
   >
-
-    <std-data-entry
-      ref="std_data_entry"
+    <StdDataEntry
       :data-list="batchColumns"
       :data-source="data"
       :error="error"
     />
 
-    <slot name="extra"/>
-  </a-modal>
+    <slot name="extra" />
+  </AModal>
 </template>
 
 <style scoped>

+ 56 - 39
app/src/components/StdDataDisplay/StdCurd.vue → app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -1,13 +1,10 @@
 <script setup lang="ts">
-import gettext from '@/gettext'
+import { provide, reactive, ref } from 'vue'
+import { message } from 'ant-design-vue'
 import StdTable from './StdTable.vue'
+import gettext from '@/gettext'
 
-import StdDataEntry from '@/components/StdDataEntry'
-
-import {provide, reactive, ref} from 'vue'
-import {message} from 'ant-design-vue'
-
-const {$gettext} = gettext
+import StdDataEntry from '@/components/StdDesign/StdDataEntry'
 
 const props = defineProps({
   api: Object,
@@ -15,55 +12,59 @@ const props = defineProps({
   title: String,
   data_key: {
     type: String,
-    default: 'data'
+    default: 'data',
   },
   disable_search: {
     type: Boolean,
-    default: false
+    default: false,
   },
   disable_add: {
     type: Boolean,
-    default: false
+    default: false,
   },
   soft_delete: {
     type: Boolean,
-    default: false
+    default: false,
   },
   edit_text: String,
   deletable: {
     type: Boolean,
-    default: true
+    default: true,
   },
   get_params: {
     type: Object,
     default() {
       return {}
-    }
+    },
   },
   editable: {
     type: Boolean,
-    default: true
+    default: true,
   },
   beforeSave: {
     type: Function,
     default: () => {
-    }
+    },
   },
   exportCsv: {
     type: Boolean,
-    default: false
+    default: false,
   },
   modalWidth: {
     type: Number,
-    default: 600
+    default: 600,
   },
-  useSortable: Boolean
+  useSortable: Boolean,
 })
 
+const { $gettext } = gettext
+
 const visible = ref(false)
 const update = ref(0)
-const data: any = reactive({id: null})
+const data: any = reactive({ id: null })
+
 provide('data', data)
+
 const error: any = reactive({})
 const selected = ref([])
 
@@ -88,13 +89,14 @@ function add() {
 
 function get_list() {
   const t: Table = table.value!
+
   t!.get_list()
 }
 
 defineExpose({
   add,
   get_list,
-  data
+  data,
 })
 
 const table = ref(null)
@@ -111,7 +113,7 @@ function clear_error() {
 
 const ok = async () => {
   clear_error()
-  await props?.beforeSave!?.(data)
+  await props?.beforeSave?.(data)
   props.api!.save(data.id, data).then((r: any) => {
     message.success($gettext('Save Successfully'))
     Object.assign(data, r)
@@ -147,50 +149,65 @@ const selectedRowKeys = ref([])
 
 <template>
   <div class="std-curd">
-    <a-card :title="title||$gettext('Table')">
-      <template v-if="!disable_add" #extra>
+    <ACard :title="title || $gettext('Table')">
+      <template
+        v-if="!disable_add"
+        #extra
+      >
         <a @click="add">{{ $gettext('Add') }}</a>
       </template>
 
-      <std-table
+      <StdTable
         ref="table"
-        v-model:selected-row-keys="selectedRowKeys"
         v-bind="props"
+        :key="update"
+        v-model:selected-row-keys="selectedRowKeys"
         @clickEdit="edit"
         @selected="onSelect"
-        :key="update"
       >
-        <template v-slot:actions="slotProps">
-          <slot name="actions" :actions="slotProps.record"/>
+        <template #actions="slotProps">
+          <slot
+            name="actions"
+            :actions="slotProps.record"
+          />
         </template>
-      </std-table>
-    </a-card>
+      </StdTable>
+    </ACard>
 
-    <a-modal
+    <AModal
       class="std-curd-edit-modal"
       :mask="false"
-      :title="edit_text?edit_text:(data.id ? $gettext('Modify') : $gettext('Add'))"
+      :title="edit_text ? edit_text : (data.id ? $gettext('Modify') : $gettext('Add'))"
       :open="visible"
       :cancel-text="$gettext('Cancel')"
       :ok-text="$gettext('OK')"
+      :width="modalWidth"
+      destroy-on-close
       @cancel="cancel"
       @ok="ok"
-      :width="modalWidth"
-      destroyOnClose
     >
-      <div class="before-edit" v-if="$slots.beforeEdit">
-        <slot name="beforeEdit" :data="data"/>
+      <div
+        v-if="$slots.beforeEdit"
+        class="before-edit"
+      >
+        <slot
+          name="beforeEdit"
+          :data="data"
+        />
       </div>
 
-      <std-data-entry
+      <StdDataEntry
         ref="std_data_entry"
         :data-list="editableColumns()"
         :data-source="data"
         :error="error"
       />
 
-      <slot name="edit" :data="data"/>
-    </a-modal>
+      <slot
+        name="edit"
+        :data="data"
+      />
+    </AModal>
   </div>
 </template>
 

+ 16 - 10
app/src/components/StdDataDisplay/StdPagination.vue → app/src/components/StdDesign/StdDataDisplay/StdPagination.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
-import {useGettext} from 'vue3-gettext'
-import {computed} from 'vue'
 
-const props = defineProps(['pagination', 'size'])
-const emit = defineEmits(['change', 'changePageSize'])
-const {$gettext} = useGettext()
+import type { Pagination } from '@/api/curd'
+
+const props = defineProps<{
+  pagination: Pagination
+  size?: 'small' | 'default'
+}>()
+
+const emit = defineEmits(['change', 'changePageSize', 'update:pagination'])
 
 function change(num: number, pageSize: number) {
   emit('change', num, pageSize)
@@ -16,16 +19,19 @@ const pageSize = computed({
   },
   set(v) {
     emit('changePageSize', v)
-    props.pagination.per_page = v
-  }
+    emit('update:pagination', { ...props.pagination, per_page: v })
+  },
 })
 </script>
 
 <template>
-  <div class="pagination-container" v-if="pagination.total>pagination.per_page">
-    <a-pagination
-      :current="pagination.current_page"
+  <div
+    v-if="pagination.total > pagination.per_page"
+    class="pagination-container"
+  >
+    <APagination
       v-model:pageSize="pageSize"
+      :current="pagination.current_page"
       :size="size"
       :total="pagination.total"
       @change="change"

+ 183 - 152
app/src/components/StdDataDisplay/StdTable.vue → app/src/components/StdDesign/StdDataDisplay/StdTable.vue

@@ -1,98 +1,100 @@
 <script setup lang="ts">
-import gettext from '@/gettext'
-import StdDataEntry from '@/components/StdDataEntry'
-import StdPagination from './StdPagination.vue'
-import {computed, onMounted, reactive, ref, watch} from 'vue'
-import {useRoute, useRouter} from 'vue-router'
-import {message} from 'ant-design-vue'
-import {downloadCsv} from '@/lib/helper'
+import { computed, onMounted, reactive, ref, toRaw, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { message } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import Sortable from 'sortablejs'
-import {HolderOutlined} from '@ant-design/icons-vue'
-import {toRaw} from '@vue/reactivity'
-
-const {$gettext, interpolate} = gettext
-
-const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys', 'clickBatchModify'])
+import { HolderOutlined } from '@ant-design/icons-vue'
+import _ from 'lodash'
+import StdPagination from './StdPagination.vue'
+import { downloadCsv } from '@/lib/helper'
+import StdDataEntry from '@/components/StdDesign/StdDataEntry'
+import gettext from '@/gettext'
 
 const props = defineProps({
   api: Object,
   columns: Array,
   data_key: {
     type: String,
-    default: 'data'
+    default: 'data',
   },
   disable_search: {
     type: Boolean,
-    default: false
+    default: false,
   },
   disable_query_params: {
     type: Boolean,
-    default: false
+    default: false,
   },
   disable_add: {
     type: Boolean,
-    default: false
+    default: false,
   },
   edit_text: String,
   deletable: {
     type: Boolean,
-    default: true
+    default: true,
   },
   get_params: {
     type: Object,
     default() {
       return {}
-    }
+    },
   },
   editable: {
     type: Boolean,
-    default: true
+    default: true,
   },
   selectionType: {
     type: String,
-    validator: function (value: string) {
-      return ['checkbox', 'radio'].indexOf(value) !== -1
-    }
+    validator(value: string) {
+      return ['checkbox', 'radio'].includes(value)
+    },
   },
   pithy: {
     type: Boolean,
-    default: false
+    default: false,
   },
   scrollX: {
     type: [Number, Boolean],
-    default: true
+    default: true,
   },
   rowKey: {
     type: String,
-    default: 'id'
+    default: 'id',
   },
   exportCsv: {
     type: Boolean,
-    default: false
+    default: false,
   },
   size: String,
   selectedRowKeys: {
-    type: Array
+    type: Array,
   },
-  useSortable: Boolean
+  useSortable: Boolean,
 })
 
+const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys', 'clickBatchModify'])
+
+const { $gettext, interpolate } = gettext
+
 const data_source: any = ref([])
 const expand_keys_list: any = ref([])
 const rows_key_index_map: any = ref({})
 
 const loading = ref(true)
+
 const pagination = reactive({
   total: 1,
   per_page: 10,
   current_page: 1,
-  total_pages: 1
+  total_pages: 1,
 })
 
 const route = useRoute()
+
 const params = reactive({
-  ...props.get_params
+  ...props.get_params,
 })
 
 const selectedKeysLocalBuffer: any = ref([])
@@ -104,7 +106,7 @@ const selectedRowKeysBuffer = computed({
   set(v) {
     selectedKeysLocalBuffer.value = v
     emit('update:selectedRowKeys', v)
-  }
+  },
 })
 
 const searchColumns = getSearchColumns()
@@ -112,24 +114,23 @@ const pithyColumns = getPithyColumns()
 const batchColumns = getBatchEditColumns()
 
 onMounted(() => {
-  if (!props.disable_query_params) {
+  if (!props.disable_query_params)
     Object.assign(params, route.query)
-  }
+
   get_list()
 
-  if (props.useSortable) {
+  if (props.useSortable)
     initSortable()
-  }
 })
 
 defineExpose({
-  get_list
+  get_list,
 })
 
 function destroy(id: any) {
   props.api!.destroy(id).then(() => {
     get_list()
-    message.success(interpolate($gettext('Delete ID: %{id}'), {id: id}))
+    message.success(interpolate($gettext('Delete ID: %{id}'), { id }))
   }).catch((e: any) => {
     message.error($gettext(e?.message ?? 'Server error'))
   })
@@ -138,8 +139,8 @@ function destroy(id: any) {
 function get_list(page_num = null, page_size = 20) {
   loading.value = true
   if (page_num) {
-    params['page'] = page_num
-    params['page_size'] = page_size
+    params.page = page_num
+    params.page_size = page_size
   }
   props.api!.get_list(params).then(async (r: any) => {
     data_source.value = r.data
@@ -149,9 +150,12 @@ function get_list(page_num = null, page_size = 20) {
         if (data && data.length > 0) {
           data.forEach((v: any) => {
             v.level = level
-            let current_index = [...total, index++]
+
+            const current_index = [...total, index++]
+
             rows_key_index_map.value[v.id] = current_index
-            if (v.children) buildIndexMap(v.children, level + 1, 0, current_index)
+            if (v.children)
+              buildIndexMap(v.children, level + 1, 0, current_index)
           })
         }
       }
@@ -159,9 +163,8 @@ function get_list(page_num = null, page_size = 20) {
       buildIndexMap(r.data)
     }
 
-    if (r.pagination !== undefined) {
+    if (r.pagination !== undefined)
       Object.assign(pagination, r.pagination)
-    }
 
     loading.value = false
   }).catch((e: any) => {
@@ -172,23 +175,22 @@ function get_list(page_num = null, page_size = 20) {
 function stdChange(pagination: any, filters: any, sorter: any) {
   if (sorter) {
     selectedRowKeysBuffer.value = []
-    params['order_by'] = sorter.field
-    params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
+    params.order_by = sorter.field
+    params.sort = sorter.order === 'ascend' ? 'asc' : 'desc'
     switch (sorter.order) {
       case 'ascend':
-        params['sort'] = 'asc'
+        params.sort = 'asc'
         break
       case 'descend':
-        params['sort'] = 'desc'
+        params.sort = 'desc'
         break
       default:
-        params['sort'] = null
+        params.sort = null
         break
     }
   }
-  if (pagination) {
+  if (pagination)
     selectedRowKeysBuffer.value = []
-  }
 }
 
 function expandedTable(keys: any) {
@@ -196,22 +198,24 @@ function expandedTable(keys: any) {
 }
 
 function getSearchColumns() {
-  let searchColumns: any = []
+  const searchColumns: any = []
+
   props.columns!.forEach((column: any) => {
-    if (column.search) {
+    if (column.search)
       searchColumns.push(column)
-    }
   })
+
   return searchColumns
 }
 
 function getBatchEditColumns() {
-  let batch: any = []
+  const batch: any = []
+
   props.columns!.forEach((column: any) => {
-    if (column.batch) {
+    if (column.batch)
       batch.push(column)
-    }
   })
+
   return batch
 }
 
@@ -221,6 +225,7 @@ function getPithyColumns() {
       return c.pithy === true && c.display !== false
     })
   }
+
   return props.columns!.filter((c: any, index: any, columns: any) => {
     return c.display !== false
   })
@@ -241,10 +246,14 @@ async function onSelectChange(_selectedRowKeys: any) {
   Object.keys(crossPageSelect).forEach(v => {
     t.push(...crossPageSelect[v])
   })
+
   const n: any = [..._selectedRowKeys]
+
   t = await t.concat(n)
+
   // console.log(crossPageSelect)
   const set = new Set(t)
+
   selectedRowKeysBuffer.value = Array.from(set)
   emit('onSelected', selectedRowKeysBuffer.value)
 }
@@ -261,66 +270,58 @@ const reset_search = async () => {
   })
 
   Object.assign(params, {
-    ...props.get_params
+    ...props.get_params,
   })
 
-  router.push({query: {}}).catch(() => {
+  router.push({ query: {} }).catch(() => {
   })
 }
 
 watch(params, () => {
-  if (!props.disable_query_params) {
-    router.push({query: params})
-  }
+  if (!props.disable_query_params)
+    router.push({ query: params })
+
   get_list()
 })
 
 const rowSelection = computed(() => {
   if (batchColumns.length > 0 || props.selectionType) {
     return {
-      selectedRowKeys: selectedRowKeysBuffer.value, onChange: onSelectChange,
-      onSelect: onSelect, type: batchColumns.length > 0 ? 'checkbox' : props.selectionType
+      selectedRowKeys: selectedRowKeysBuffer.value,
+      onChange: onSelectChange,
+      onSelect,
+      type: batchColumns.length > 0 ? 'checkbox' : props.selectionType,
     }
-  } else {
+  }
+  else {
     return null
   }
 })
 
-function fn(obj: Object, desc: string) {
-  const arr: string[] = desc.split('.')
-  while (arr.length) {
-    // @ts-ignore
-    const top = obj[arr.shift()]
-    if (top === undefined) {
-      return null
-    }
-    obj = top
-  }
-  return obj
-}
+const fn = _.get
 
 async function export_csv() {
-  let header = []
-  let headerKeys: any[] = []
+  const header = []
+  const headerKeys: any[] = []
   const showColumnsMap: any = {}
-  // @ts-ignore
-  for (let showColumnsKey in pithyColumns) {
-    // @ts-ignore
-    if (pithyColumns[showColumnsKey].dataIndex === 'action') continue
-    // @ts-ignore
+
+  for (const showColumnsKey in pithyColumns) {
+    if (pithyColumns[showColumnsKey].dataIndex === 'action')
+      continue
+
     let t = pithyColumns[showColumnsKey].title
 
-    if (typeof t === 'function') {
+    if (typeof t === 'function')
       t = t()
-    }
+
     header.push({
       title: t,
-      // @ts-ignore
-      key: pithyColumns[showColumnsKey].dataIndex
+
+      key: pithyColumns[showColumnsKey].dataIndex,
     })
-    // @ts-ignore
+
     headerKeys.push(pithyColumns[showColumnsKey].dataIndex)
-    // @ts-ignore
+
     showColumnsMap[pithyColumns[showColumnsKey].dataIndex] = pithyColumns[showColumnsKey]
   }
 
@@ -329,30 +330,32 @@ async function export_csv() {
   let page = 1
   while (hasMore) {
     // 准备 DataSource
-    await props.api!.get_list({page}).then((response: any) => {
+    await props.api!.get_list({ page }).then((response: any) => {
       if (response.data.length === 0) {
         hasMore = false
+
         return
       }
-      if (response[props.data_key] === undefined) {
+      if (response[props.data_key] === undefined)
         dataSource = dataSource.concat(...response.data)
-      } else {
+      else
         dataSource = dataSource.concat(...response[props.data_key])
-      }
     }).catch((e: any) => {
       message.error(e.message ?? $gettext('Server error'))
       hasMore = false
-      return
     })
     page += 1
   }
   const data: any[] = []
+
   dataSource.forEach((row: Object) => {
-    let obj: any = {}
+    const obj: any = {}
+
     headerKeys.forEach(key => {
       let data = fn(row, key)
       const c = showColumnsMap[key]
-      data = c?.customRender?.({text: data}) ?? data
+
+      data = c?.customRender?.({ text: data }) ?? data
       obj[c.dataIndex] = data
     })
     data.push(obj)
@@ -375,61 +378,65 @@ function getLeastIndex(index: number) {
 }
 
 function getTargetData(data: any, indexList: number[]): any {
-  let target: any = {children: data}
+  let target: any = { children: data }
   indexList.forEach((index: number) => {
     target.children[index].parent = target
     target = target.children[index]
   })
+
   return target
 }
 
 function initSortable() {
   const table: any = document.querySelector('#std-table tbody')
+
   new Sortable(table, {
     handle: '.ant-table-drag-icon',
     animation: 150,
     sort: true,
     forceFallback: true,
-    setData: function (dataTransfer) {
+    setData(dataTransfer) {
       dataTransfer.setData('Text', '')
     },
-    onStart({item}) {
-      let targetRowKey = Number(item.dataset.rowKey)
-      if (targetRowKey) {
+    onStart({ item }) {
+      const targetRowKey = Number(item.dataset.rowKey)
+      if (targetRowKey)
         expand_keys_list.value = expand_keys_list.value.filter((item: number) => item !== targetRowKey)
-      }
     },
-    onMove({dragged, related}) {
+    onMove({ dragged, related }) {
       const oldRow: number[] = rows_key_index_map.value?.[Number(dragged.dataset.rowKey)]
       const newRow: number[] = rows_key_index_map.value?.[Number(related.dataset.rowKey)]
-      if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] != newRow[newRow.length - 2]) {
+      if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] != newRow[newRow.length - 2])
         return false
-      }
     },
-    async onEnd({item, newIndex, oldIndex}) {
-      if (newIndex === oldIndex) return
+    async onEnd({ item, newIndex, oldIndex }) {
+      if (newIndex === oldIndex)
+        return
 
       const indexDelta: number = Number(oldIndex) - Number(newIndex)
       const direction: number = indexDelta > 0 ? +1 : -1
 
-      let rowIndex: number[] = rows_key_index_map.value?.[Number(item.dataset.rowKey)]
+      const rowIndex: number[] = rows_key_index_map.value?.[Number(item.dataset.rowKey)]
       const newRow = getTargetData(data_source.value, rowIndex)
       const newRowParent = newRow.parent
       const level: number = newRow.level
 
-      let currentRowIndex: number[] = [...rows_key_index_map.value?.
+      const currentRowIndex: number[] = [...rows_key_index_map.value?.
         [Number(table.children[Number(newIndex) + direction].dataset.rowKey)]]
-      let currentRow: any = getTargetData(data_source.value, currentRowIndex)
+
+      const currentRow: any = getTargetData(data_source.value, currentRowIndex)
+
       // Reset parent
       currentRow.parent = newRow.parent = null
       newRowParent.children.splice(rowIndex[level], 1)
       newRowParent.children.splice(currentRowIndex[level], 0, toRaw(newRow))
 
-      let changeIds: number[] = []
+      const changeIds: number[] = []
 
       function processChanges(row: any, children: boolean = false, newIndex: number | undefined = undefined) {
         // Build changes ID list expect new row
-        if (children || newIndex === undefined) changeIds.push(row.id)
+        if (children || newIndex === undefined)
+          changeIds.push(row.id)
 
         if (newIndex !== undefined)
           rows_key_index_map.value[row.id][level] = newIndex
@@ -437,16 +444,17 @@ function initSortable() {
           rows_key_index_map.value[row.id][level] += direction
 
         row.parent = null
-        if (row.children) {
+        if (row.children)
           row.children.forEach((v: any) => processChanges(v, true, newIndex))
-        }
       }
 
       // Replace row index for new row
       processChanges(newRow, false, currentRowIndex[level])
+
       // Rebuild row index maps for changes row
       for (let i = Number(oldIndex); i != newIndex; i -= direction) {
-        let rowIndex: number[] = rows_key_index_map.value?.[table.children[i].dataset.rowKey]
+        const rowIndex: number[] = rows_key_index_map.value?.[table.children[i].dataset.rowKey]
+
         rowIndex[level] += direction
         processChanges(getTargetData(data_source.value, rowIndex))
       }
@@ -455,83 +463,106 @@ function initSortable() {
 
       props.api!.update_order({
         target_id: newRow.id,
-        direction: direction,
-        affected_ids: changeIds
+        direction,
+        affected_ids: changeIds,
       }).then(() => {
         message.success($gettext('Updated successfully'))
       }).catch((e: any) => {
         message.error(e?.message ?? $gettext('Server error'))
       })
-    }
+    },
   })
 }
 
-
 </script>
 
 <template>
   <div class="std-table">
-    <std-data-entry
+    <StdDataEntry
       v-if="!disable_search && searchColumns.length"
       :data-list="searchColumns"
       :data-source="params"
       layout="inline"
     >
       <template #action>
-        <a-space class="action-btn">
-          <a-button v-if="exportCsv" @click="export_csv" type="primary" ghost>
+        <ASpace class="action-btn">
+          <AButton
+            v-if="exportCsv"
+            type="primary"
+            ghost
+            @click="export_csv"
+          >
             {{ $gettext('Export') }}
-          </a-button>
-          <a-button @click="reset_search">
+          </AButton>
+          <AButton @click="reset_search">
             {{ $gettext('Reset') }}
-          </a-button>
-          <a-button v-if="hasSelectedRow" @click="click_batch_edit">
+          </AButton>
+          <AButton
+            v-if="hasSelectedRow"
+            @click="click_batch_edit"
+          >
             {{ $gettext('Batch Modify') }}
-          </a-button>
-        </a-space>
+          </AButton>
+        </ASpace>
       </template>
-    </std-data-entry>
-    <a-table
+    </StdDataEntry>
+    <ATable
+      id="std-table"
       :columns="pithyColumns"
       :data-source="data_source"
       :loading="loading"
       :pagination="false"
       :row-key="rowKey"
-      :rowSelection="rowSelection"
-      @change="stdChange"
+      :row-selection="rowSelection"
       :scroll="{ x: scrollX }"
       :size="size"
-      id="std-table"
+      :expanded-row-keys="expand_keys_list"
+      @change="stdChange"
       @expandedRowsChange="expandedTable"
-      :expandedRowKeys="expand_keys_list"
     >
-      <template
-        v-slot:bodyCell="{text, record, index, column}"
-      >
+      <template #bodyCell="{ text, record, index, column }">
         <template v-if="column.handle === true">
-          <span class="ant-table-drag-icon"><HolderOutlined/></span>
+          <span class="ant-table-drag-icon"><HolderOutlined /></span>
           {{ text }}
         </template>
         <template v-if="column.dataIndex === 'action'">
-          <a-button type="link" size="small" v-if="props.editable"
-                    @click="$emit('clickEdit', record[props.rowKey], record)">
+          <AButton
+            v-if="props.editable"
+            type="link"
+            size="small"
+            @click="$emit('clickEdit', record[props.rowKey], record)"
+          >
             {{ props.edit_text || $gettext('Modify') }}
-          </a-button>
-          <slot name="actions" :record="record"/>
+          </AButton>
+          <slot
+            name="actions"
+            :record="record"
+          />
           <template v-if="props.deletable">
-            <a-divider type="vertical"/>
-            <a-popconfirm
-              :cancelText="$gettext('No')"
-              :okText="$gettext('OK')"
+            <ADivider type="vertical" />
+            <APopconfirm
+              :cancel-text="$gettext('No')"
+              :ok-text="$gettext('OK')"
               :title="$gettext('Are you sure you want to delete?')"
-              @confirm="destroy(record[rowKey])">
-              <a-button type="link" size="small">{{ $gettext('Delete') }}</a-button>
-            </a-popconfirm>
+              @confirm="destroy(record[rowKey])"
+            >
+              <AButton
+                type="link"
+                size="small"
+              >
+                {{ $gettext('Delete') }}
+              </AButton>
+            </APopconfirm>
           </template>
         </template>
       </template>
-    </a-table>
-    <std-pagination :size="size" :pagination="pagination" @change="get_list" @changePageSize="stdChange"/>
+    </ATable>
+    <StdPagination
+      :size="size"
+      :pagination="pagination"
+      @change="get_list"
+      @changePageSize="stdChange"
+    />
   </div>
 </template>
 

+ 8 - 6
app/src/components/StdDataDisplay/StdTableTransformer.tsx → app/src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx

@@ -2,10 +2,13 @@
 import dayjs from 'dayjs'
 
 export interface customRender {
-  value: any
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   text: any
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   record: any
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   index: any
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   column: any
 }
 
@@ -16,17 +19,16 @@ export const datetime = (args: customRender) => {
 export const date = (args: customRender) => {
   return dayjs(args.text).format('YYYY-MM-DD')
 }
-
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const mask = (args: customRender, maskObj: any) => {
   let v
 
-  if (typeof maskObj?.[args.text] === 'function') {
+  if (typeof maskObj?.[args.text] === 'function')
     v = maskObj[args.text]()
-  } else if (typeof maskObj?.[args.text] === 'string') {
+  else if (typeof maskObj?.[args.text] === 'string')
     v = maskObj[args.text]
-  } else {
+  else
     v = args.text
-  }
 
   return <div>{v}</div>
 }

+ 0 - 0
app/src/components/StdDataDisplay/index.ts → app/src/components/StdDesign/StdDataDisplay/index.ts


+ 54 - 0
app/src/components/StdDesign/StdDataEntry/StdDataEntry.tsx

@@ -0,0 +1,54 @@
+import { defineComponent } from 'vue'
+import { Form } from 'ant-design-vue'
+import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
+import './style.less'
+
+export default defineComponent({
+  props: {
+    dataList: {
+      type: Array,
+      required: true,
+    },
+    dataSource: {
+      type: Object,
+      required: true,
+    },
+    error: {
+      type: Object,
+      required: false,
+    },
+    layout: {
+      type: String,
+      required: false,
+    },
+  },
+  emits: ['update:dataSource'],
+  setup(props, { slots }) {
+    return () => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const template: any = []
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      props.dataList.forEach((v: any) => {
+        let show = true
+        if (v.edit.show) {
+          if (typeof v.edit.show === 'boolean')
+            show = v.edit.show
+          else if (typeof v.edit.show === 'function')
+            show = v.edit.show(props.dataSource)
+        }
+        if (v.edit.type && show) {
+          template.push(<StdFormItem dataIndex={v.dataIndex} label={v.title()} extra={v.extra} error={props.error}>
+            {v.edit.type(v.edit, props.dataSource, v.dataIndex)}
+          </StdFormItem>,
+          )
+        }
+      })
+
+      if (slots.action)
+        template.push(<div class={'std-data-entry-action'}>{slots.action()}</div>)
+
+      return <Form layout={props.layout || 'vertical'}>{template}</Form>
+    }
+  },
+})

+ 20 - 14
app/src/components/StdDataEntry/StdFormItem.vue → app/src/components/StdDesign/StdDataEntry/StdFormItem.vue

@@ -1,43 +1,49 @@
 <script setup lang="ts">
-import {computed} from 'vue'
-import {useGettext} from 'vue3-gettext'
+import { computed } from 'vue'
+import { useGettext } from 'vue3-gettext'
 
-const {$gettext} = useGettext()
+const props = defineProps<Props>()
+
+const { $gettext } = useGettext()
 
 export interface Props {
   dataIndex?: string
   label?: string
   extra?: string
-  error?: any
+  error?: {
+    [key: string]: string
+  }
 }
 
-const props = defineProps<Props>()
-
 const tag = computed(() => {
   return props.error?.[props.dataIndex] ?? ''
 })
 
 const valid_status = computed(() => {
-  if (!!tag.value) {
+  if (tag.value)
     return 'error'
-  } else {
+  else
     return 'success'
-  }
 })
 
 const help = computed(() => {
-  if (tag.value.indexOf('required') > -1) {
+  if (tag.value.includes('required'))
     return () => $gettext('This field should not be empty')
-  }
+
   return () => {
   }
 })
 </script>
 
 <template>
-  <a-form-item :label="label" :extra="extra" :validate-status="valid_status" :help="help?.()">
-    <slot/>
-  </a-form-item>
+  <AFormItem
+    :label="label"
+    :extra="extra"
+    :validate-status="valid_status"
+    :help="help?.()"
+  >
+    <slot />
+  </AFormItem>
 </template>
 
 <style scoped lang="less">

+ 32 - 14
app/src/components/StdDataEntry/components/StdPassword.vue → app/src/components/StdDesign/StdDataEntry/components/StdPassword.vue

@@ -1,7 +1,13 @@
 <script setup lang="ts">
-import {computed, ref} from 'vue'
+import { computed, ref } from 'vue'
+import { useGettext } from 'vue3-gettext'
+
+const props = defineProps<{
+  value: string
+  generate: boolean
+  placeholder: string
+}>()
 
-const props = defineProps(['value', 'generate', 'placeholder'])
 const emit = defineEmits(['update:value'])
 
 const M_value = computed({
@@ -10,10 +16,11 @@ const M_value = computed({
   },
   set(v) {
     emit('update:value', v)
-  }
+  },
 })
-const visibility = ref(false)
 
+const visibility = ref(false)
+const { $gettext } = useGettext()
 function handle_generate() {
   visibility.value = true
   M_value.value = 'xxxx'
@@ -23,25 +30,36 @@ function handle_generate() {
   let password = ''
   for (let i = 0; i <= passwordLength; i++) {
     const randomNumber = Math.floor(Math.random() * chars.length)
+
     password += chars.substring(randomNumber, randomNumber + 1)
   }
 
   M_value.value = password
-
 }
 </script>
 
 <template>
-  <a-input-group compact>
-    <a-input-password
+  <AInputGroup compact>
+    <AInputPassword
       v-if="!visibility"
-      :class="{compact: generate}"
-      v-model:value="M_value" :placeholoder="placeholder"/>
-    <a-input v-else :class="{compact: generate}" v-model:value="M_value" :placeholoder="placeholder"/>
-    <a-button @click="handle_generate" v-if="generate" type="primary">
-      <translate>Generate</translate>
-    </a-button>
-  </a-input-group>
+      v-model:value="M_value"
+      :class="{ compact: generate }"
+      :placeholoder="placeholder"
+    />
+    <AInput
+      v-else
+      v-model:value="M_value"
+      :class="{ compact: generate }"
+      :placeholoder="placeholder"
+    />
+    <AButton
+      v-if="generate"
+      type="primary"
+      @click="handle_generate"
+    >
+      {{ $gettext('Generate') }}
+    </AButton>
+  </AInputGroup>
 </template>
 
 <style scoped>

+ 53 - 0
app/src/components/StdDesign/StdDataEntry/components/StdSelect.vue

@@ -0,0 +1,53 @@
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import type { SelectProps } from 'ant-design-vue'
+
+const props = defineProps<{
+  value: string
+  mask: Record<string, string | (() => string)>
+}>()
+
+const emit = defineEmits(['update:value'])
+
+const options = computed(() => {
+  const _options = ref<SelectProps['options']>([])
+
+  for (const [key, value] of Object.entries(props.mask)) {
+    const v = value as () => string
+
+    _options.value!.push({ label: v?.(), value: key })
+  }
+
+  return _options
+})
+
+const _value = computed({
+  get() {
+    let v
+
+    if (typeof props.mask?.[props.value] === 'function')
+      v = (props.mask[props.value] as () => string)()
+    else if (typeof props.mask?.[props.value] === 'string')
+      v = props.mask[props.value]
+    else
+      v = props.value
+
+    return v
+  },
+  set(v) {
+    emit('update:value', v)
+  },
+})
+</script>
+
+<template>
+  <ASelect
+    v-model:value="_value"
+    :options="options.value"
+    style="min-width: 180px"
+  />
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 53 - 39
app/src/components/StdDataEntry/components/StdSelector.vue → app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue

@@ -1,13 +1,25 @@
 <script setup lang="ts">
-import {computed, onMounted, reactive, ref, watch} from 'vue'
-import StdTable from '@/components/StdDataDisplay/StdTable.vue'
+import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import gettext from '@/gettext'
+import type Curd from '@/api/curd'
+
+const props = defineProps<{
+  selectedKey: string | number
+  value: string | number
+  recordValueIndex: string
+  selectionType: 'radio' | 'checkbox'
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  api: Curd<any>
+  columns: any[]
+  dataKey: string
+  disableSearch: boolean
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  getParams: any
+  description: string
+}>()
 
-const {$gettext} = gettext
-const props = defineProps(['selectedKey', 'value', 'recordValueIndex',
-  'selectionType', 'api', 'columns', 'data_key',
-  'disable_search', 'get_params', 'description'])
 const emit = defineEmits(['update:selectedKey', 'changeSelect'])
+const { $gettext } = gettext
 const visible = ref(false)
 const M_value = ref('')
 
@@ -17,10 +29,11 @@ onMounted(() => {
 
 const selected = ref([])
 
-const record: any = reactive({})
+const record = reactive({})
 
 function init() {
   if (props.selectedKey && !props.value && props.selectionType === 'radio') {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     props.api.get(props.selectedKey).then((r: any) => {
       Object.assign(record, r)
       M_value.value = r[props.recordValueIndex]
@@ -31,34 +44,33 @@ function init() {
 function show() {
   visible.value = true
 }
-
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 function onSelect(_selected: any) {
   selected.value = _selected
 }
-
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 function onSelectedRecord(r: any) {
   Object.assign(record, r)
 }
 
 function ok() {
   visible.value = false
-  if (props.selectionType == 'radio') {
+  if (props.selectionType === 'radio')
     emit('update:selectedKey', selected.value[0])
-  } else {
+  else
     emit('update:selectedKey', selected.value)
-  }
+
   M_value.value = record[props.recordValueIndex]
   emit('changeSelect', record)
 }
 
 watch(props, () => {
-  if (!props?.selectedKey) {
+  if (!props?.selectedKey)
     M_value.value = ''
-  } else if (props.value) {
-    M_value.value = props.value
-  } else {
+  else if (props.value)
+    M_value.value = props.value as string
+  else
     init()
-  }
 })
 
 const _selectedKey = computed({
@@ -67,48 +79,54 @@ const _selectedKey = computed({
   },
   set(v) {
     emit('update:selectedKey', v)
-  }
+  },
 })
 </script>
 
 <template>
   <div class="std-selector-container">
-    <div class="std-selector" @click="show()">
-      <a-input v-model="_selectedKey" disabled hidden/>
+    <div
+      class="std-selector"
+      @click="show"
+    >
+      <AInput
+        v-model="_selectedKey"
+        disabled
+        hidden
+      />
       <div class="value">
         {{ M_value }}
       </div>
-      <a-modal
+      <AModal
         :mask="false"
         :open="visible"
         :cancel-text="$gettext('Cancel')"
         :ok-text="$gettext('OK')"
         :title="$gettext('Selector')"
-        @cancel="visible=false"
-        @ok="ok()"
         :width="800"
-        destroyOnClose
+        destroy-on-close
+        @cancel="visible = false"
+        @ok="ok"
       >
         {{ description }}
-        <std-table
+        <StdTable
           :api="api"
           :columns="columns"
-          :data_key="data_key"
-          :disable_search="disable_search"
-          :pithy="true"
-          :get_params="get_params"
-          :selectionType="selectionType"
-          :disable_query_params="true"
-          @onSelected="onSelect"
-          @onSelectedRecord="onSelectedRecord"
+          :data_key="dataKey"
+          :disable_search="disableSearch"
+          pithy
+          :get_params="getParams"
+          :selection-type="selectionType"
+          disable_query_params
+          @on-selected="onSelect"
+          @on-selected-record="onSelectedRecord"
         />
-      </a-modal>
+      </AModal>
     </div>
   </div>
 </template>
 
 <style lang="less" scoped>
-.dark .std-selector-container
 .std-selector-container {
   height: 39.9px;
   display: flex;
@@ -132,10 +150,6 @@ const _selectedKey = computed({
     margin: 0 10px 0 0;
     cursor: pointer;
     min-width: 180px;
-
-    .value {
-
-    }
   }
 }
 </style>

+ 101 - 0
app/src/components/StdDesign/StdDataEntry/index.tsx

@@ -0,0 +1,101 @@
+import { h } from 'vue'
+import { Input, InputNumber, Switch, Textarea } from 'ant-design-vue'
+import _ from 'lodash'
+import StdDataEntry from './StdDataEntry'
+import StdSelector from './components/StdSelector.vue'
+import StdSelect from './components/StdSelect.vue'
+import StdPassword from './components/StdPassword.vue'
+import type { StdDesignEdit } from '@/components/StdDesign/types'
+
+const fn = _.get
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function readonly(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return h('p', fn(dataSource, dataIndex))
+}
+
+function placeholder_helper(edit: StdDesignEdit) {
+  return typeof edit.config?.placeholder === 'function' ? edit.config?.placeholder() : edit.config?.placeholder
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function input(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return h(Input, {
+    'placeholder': placeholder_helper(edit),
+    'value': dataSource?.[dataIndex],
+    'onUpdate:value': value => {
+      dataSource[dataIndex] = value
+    },
+  })
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function inputNumber(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return h(InputNumber, {
+    'placeholder': placeholder_helper(edit),
+    'min': edit.config?.min,
+    'max': edit.config?.max,
+    'value': dataSource?.[dataIndex],
+    'onUpdate:value': value => {
+      dataSource[dataIndex] = value
+    },
+  })
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function textarea(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return h(Textarea, {
+    'placeholder': placeholder_helper(edit),
+    'value': dataSource?.[dataIndex],
+    'onUpdate:value': value => {
+      dataSource[dataIndex] = value
+    },
+  })
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function password(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return <StdPassword
+    v-model:value={dataSource[dataIndex]}
+    generate={edit.config?.generate}
+    placeholder={placeholder_helper(edit)}
+  />
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function select(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return <StdSelect
+    v-model:value={dataSource[dataIndex]}
+    mask={edit.mask}
+  />
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function selector(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return <StdSelector
+    v-model:selectedKey={dataSource[dataIndex]}
+    recordValueIndex={edit.selector?.recordValueIndex}
+    selectionType={edit.selector?.selectionType}
+    api={edit.selector?.api}
+    columns={edit.selector?.columns}
+    disableSearch={edit.selector?.disable_search}
+    getParams={edit.selector?.get_params}
+    description={edit.selector?.description}
+  />
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function switcher(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return h(Switch, {
+    'checked': dataSource?.[dataIndex],
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    'onUpdate:checked': (value: any) => {
+      dataSource[dataIndex] = value
+    },
+  })
+}
+
+export {
+  readonly,
+  input,
+  textarea,
+  select,
+  selector,
+  password,
+  inputNumber,
+  switcher,
+}
+
+export default StdDataEntry

+ 0 - 0
app/src/components/StdDataEntry/style.less → app/src/components/StdDesign/StdDataEntry/style.less


+ 49 - 0
app/src/components/StdDesign/types.d.ts

@@ -0,0 +1,49 @@
+import Curd from '@/api/curd'
+import {IKeyEvt} from '@/components/StdDesign/StdDataDisplay/types'
+import {Ref} from 'vue'
+
+export interface StdDesignEdit {
+  type?: function // component type
+
+  mask?: {
+    [key: string]: () => string
+  } // use for select-option
+
+  rules?: [] // validator rules
+
+  selector?: {
+    get_params?: {}
+    recordValueIndex: any // relative to api return
+    selectionType: any
+    api: Curd,
+    valueApi?: Curd,
+    columns: any
+    disable_search?: boolean
+    description?: string
+    bind?: any
+    itemKey?: any // default is id
+    dataSourceValueIndex?: any // relative to dataSource
+  } // StdSelector Config
+
+  config?: {
+    label?: string | (() => string) // label for form item
+    size?: string // class size of Std image upload
+    placeholder?: string | (() => string) // placeholder for input
+    generate?: boolean // generate btn for StdPassword
+    min?: number // min value for input number
+    max?: number // max value for input number
+    error_messages?: Ref
+    hint?: string | (() => string) // hint form item
+  }
+
+  flex?: Flex
+}
+
+
+export interface Flex {
+  sm?: string | number | boolean
+  md?: string | number | boolean
+  lg?: string | number | boolean
+  xl?: string | number | boolean
+  xxl?: string | number | boolean
+}

+ 12 - 13
app/src/components/SwitchAppearance/SwitchAppearance.vue

@@ -1,12 +1,13 @@
 <script lang="ts" setup>
-import {computed, inject, Ref} from 'vue'
-import VPSwitch from '@/components/VPSwitch/VPSwitch.vue'
+import type { Ref } from 'vue'
+import { computed, inject } from 'vue'
+import { useGettext } from 'vue3-gettext'
 import VPIconMoon from './icons/VPIconMoon.vue'
 import VPIconSun from './icons/VPIconSun.vue'
-import {useSettingsStore} from '@/pinia'
-import {useGettext} from 'vue3-gettext'
+import VPSwitch from '@/components/VPSwitch/VPSwitch.vue'
+import { useSettingsStore } from '@/pinia'
 
-const {$gettext} = useGettext()
+const { $gettext } = useGettext()
 
 const settings = useSettingsStore()
 const devicePrefersTheme = inject('devicePrefersTheme') as Ref<string>
@@ -17,17 +18,15 @@ const switchTitle = computed(() => {
 })
 
 async function toggleAppearance() {
-  if (isDark.value) {
+  if (isDark.value)
     settings.set_theme('light')
-  } else {
+  else
     settings.set_theme('dark')
-  }
 
-  if (devicePrefersTheme.value === settings.theme) {
+  if (devicePrefersTheme.value === settings.theme)
     settings.set_preference_theme('auto')
-  } else {
+  else
     settings.set_preference_theme(settings.theme)
-  }
 }
 </script>
 
@@ -38,8 +37,8 @@ async function toggleAppearance() {
     :aria-checked="isDark"
     @click="toggleAppearance"
   >
-    <VPIconSun class="sun"/>
-    <VPIconMoon class="moon"/>
+    <VPIconSun class="sun" />
+    <VPIconMoon class="moon" />
   </VPSwitch>
 </template>
 

+ 7 - 3
app/src/components/SwitchAppearance/icons/VPIconMoon.vue

@@ -1,6 +1,10 @@
 <template>
-  <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
-    <path
-      d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"/>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    aria-hidden="true"
+    focusable="false"
+    viewBox="0 0 24 24"
+  >
+    <path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z" />
   </svg>
 </template>

+ 15 - 15
app/src/components/SwitchAppearance/icons/VPIconSun.vue

@@ -1,18 +1,18 @@
 <template>
-  <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
-    <path
-      d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z"/>
-    <path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z"/>
-    <path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z"/>
-    <path
-      d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z"/>
-    <path
-      d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z"/>
-    <path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z"/>
-    <path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z"/>
-    <path
-      d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z"/>
-    <path
-      d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z"/>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    aria-hidden="true"
+    focusable="false"
+    viewBox="0 0 24 24"
+  >
+    <path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z" />
+    <path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z" />
+    <path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z" />
+    <path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z" />
+    <path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z" />
+    <path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z" />
+    <path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z" />
+    <path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z" />
+    <path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z" />
   </svg>
 </template>

+ 10 - 3
app/src/components/VPSwitch/VPSwitch.vue

@@ -1,8 +1,15 @@
 <template>
-  <button class="VPSwitch" type="button" role="switch">
+  <button
+    class="VPSwitch"
+    type="button"
+    role="switch"
+  >
     <span class="check">
-      <span class="icon" v-if="$slots.default">
-        <slot/>
+      <span
+        v-if="$slots.default"
+        class="icon"
+      >
+        <slot />
       </span>
     </span>
   </button>

+ 2 - 2
app/src/gettext.ts

@@ -1,11 +1,11 @@
-import {createGettext} from 'vue3-gettext'
+import { createGettext } from 'vue3-gettext'
 import i18n from '../i18n.json'
 
 export default createGettext({
   availableLanguages: i18n,
   defaultLanguage: 'en',
   translations: {},
-  silent: true
+  silent: true,
 })
 
 export class useGettext {

+ 2 - 2
app/src/language/constants.ts

@@ -1,6 +1,6 @@
 import gettext from '@/gettext'
 
-const {$gettext} = gettext
+const { $gettext } = gettext
 
 export const msg = [
   $gettext('The username or password is incorrect'),
@@ -32,5 +32,5 @@ export const msg = [
   $gettext('Upgraded successfully'),
 
   $gettext('File exists'),
-  $gettext('Requested with wrong parameters')
+  $gettext('Requested with wrong parameters'),
 ]

+ 49 - 44
app/src/layouts/BaseLayout.vue

@@ -1,17 +1,17 @@
 <script setup lang="ts">
-import HeaderLayout from './HeaderLayout.vue'
-import SideBar from './SideBar.vue'
-import FooterLayout from './FooterLayout.vue'
-import PageHeader from '@/components/PageHeader/PageHeader.vue'
 import zh_CN from 'ant-design-vue/es/locale/zh_CN'
 import zh_TW from 'ant-design-vue/es/locale/zh_TW'
 import en_US from 'ant-design-vue/es/locale/en_US'
-import {computed, ref} from 'vue'
-import {theme} from 'ant-design-vue'
+import { computed, ref } from 'vue'
+import { theme } from 'ant-design-vue'
 import _ from 'lodash'
+import FooterLayout from './FooterLayout.vue'
+import SideBar from './SideBar.vue'
+import HeaderLayout from './HeaderLayout.vue'
+import PageHeader from '@/components/PageHeader/PageHeader.vue'
 
 import gettext from '@/gettext'
-import {useSettingsStore} from '@/pinia'
+import { useSettingsStore } from '@/pinia'
 
 const drawer_visible = ref(false)
 const collapsed = ref(collapse())
@@ -38,60 +38,67 @@ const lang = computed(() => {
       return en_US
   }
 })
+
 const settings = useSettingsStore()
-const is_theme_dark = computed(() => settings.theme == 'dark')
+const is_theme_dark = computed(() => settings.theme === 'dark')
 </script>
 
 <template>
-  <a-config-provider :theme="{
-      algorithm: is_theme_dark?theme.darkAlgorithm:theme.defaultAlgorithm,
-    }" :locale="lang" :autoInsertSpaceInButton="false">
-    <a-layout style="min-height: 100vh">
+  <AConfigProvider
+    :theme="{
+      algorithm: is_theme_dark ? theme.darkAlgorithm : theme.defaultAlgorithm,
+    }"
+    :locale="lang"
+    :auto-insert-space-in-button="false"
+  >
+    <ALayout style="min-height: 100vh">
       <div class="drawer-sidebar">
-        <a-drawer
-          :closable="false"
+        <ADrawer
           v-model:open="drawer_visible"
+          :closable="false"
           placement="left"
-          @close="drawer_visible=false"
           width="256"
+          @close="drawer_visible = false"
         >
-          <side-bar/>
-        </a-drawer>
+          <SideBar />
+        </ADrawer>
       </div>
 
-      <a-layout-sider
+      <ALayoutSider
         v-model:collapsed="collapsed"
-        :collapsible="true"
-        :style="{zIndex: 11}"
+        collapsible
+        :style="{ zIndex: 11 }"
         theme="light"
         class="layout-sider"
       >
-        <side-bar/>
-      </a-layout-sider>
+        <SideBar />
+      </ALayoutSider>
 
-      <a-layout>
-        <a-layout-header :style="{position: 'sticky', top: '0', zIndex: 10, width:'100%'}">
-          <header-layout @clickUnFold="drawer_visible=true"/>
-        </a-layout-header>
+      <ALayout>
+        <ALayoutHeader :style="{ position: 'sticky', top: '0', zIndex: 10, width: '100%' }">
+          <HeaderLayout @click-un-fold="drawer_visible = true" />
+        </ALayoutHeader>
 
-        <a-layout-content>
-          <page-header/>
+        <ALayoutContent>
+          <PageHeader />
           <div class="router-view">
-            <router-view v-slot="{ Component, route }">
-              <transition name="slide-fade">
-                <component :is="Component" :key="route.path"/>
-              </transition>
-            </router-view>
+            <RouterView v-slot="{ Component, route }">
+              <Transition name="slide-fade">
+                <component
+                  :is="Component"
+                  :key="route.path"
+                />
+              </Transition>
+            </RouterView>
           </div>
-        </a-layout-content>
-
-        <a-layout-footer>
-          <footer-layout/>
-        </a-layout-footer>
-      </a-layout>
-
-    </a-layout>
-  </a-config-provider>
+        </ALayoutContent>
+
+        <ALayoutFooter>
+          <FooterLayout />
+        </ALayoutFooter>
+      </ALayout>
+    </ALayout>
+  </AConfigProvider>
 </template>
 
 <style lang="less" scoped>
@@ -195,7 +202,6 @@ body {
   background-color: #fff !important;
 }
 
-
 .ant-layout-sider {
   background-color: #ffffff;
 
@@ -216,7 +222,6 @@ body {
   }
 }
 
-
 .ant-table-small {
   font-size: 13px;
 }

+ 5 - 5
app/src/layouts/BaseRouterView.vue

@@ -1,13 +1,13 @@
-<template>
-  <router-view/>
-</template>
-
 <script>
 export default {
-  name: 'BaseRouterView'
+  name: 'BaseRouterView',
 }
 </script>
 
+<template>
+  <RouterView />
+</template>
+
 <style scoped>
 
 </style>

+ 4 - 11
app/src/layouts/FooterLayout.vue

@@ -1,20 +1,13 @@
+<script setup lang="ts">
+const thisYear = computed(() => new Date().getFullYear())
+</script>
+
 <template>
   <div class="footer center">
     Copyright © 2020 - {{ thisYear }} Nginx UI
   </div>
 </template>
 
-<script>
-export default {
-  name: 'FooterComponent',
-  data() {
-    return {
-      thisYear: new Date().getFullYear()
-    }
-  }
-}
-</script>
-
 <style scoped>
 .footer {
 

+ 19 - 13
app/src/layouts/HeaderLayout.vue

@@ -1,14 +1,18 @@
 <script setup lang="ts">
+import { message } from 'ant-design-vue'
+import { HomeOutlined, LogoutOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue'
+import { useRouter } from 'vue-router'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import gettext from '@/gettext'
-import {message} from 'ant-design-vue'
 import auth from '@/api/auth'
-import {HomeOutlined, LogoutOutlined, MenuUnfoldOutlined} from '@ant-design/icons-vue'
-import {useRouter} from 'vue-router'
 import NginxControl from '@/components/NginxControl/NginxControl.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
 
-const {$gettext} = gettext
+const emit = defineEmits<{
+  clickUnFold: () => void
+}>()
+
+const { $gettext } = gettext
 
 const router = useRouter()
 
@@ -24,28 +28,30 @@ function logout() {
 <template>
   <div class="header">
     <div class="tool">
-      <MenuUnfoldOutlined @click="$emit('clickUnFold')"/>
+      <MenuUnfoldOutlined @click="emit('clickUnFold')" />
     </div>
 
-    <a-space class="user-wrapper" :size="24">
-      <SetLanguage class="set_lang"/>
+    <ASpace
+      class="user-wrapper"
+      :size="24"
+    >
+      <SetLanguage class="set_lang" />
 
-      <SwitchAppearance/>
+      <SwitchAppearance />
 
       <a href="/">
-        <HomeOutlined/>
+        <HomeOutlined />
       </a>
 
-      <NginxControl/>
+      <NginxControl />
 
       <a @click="logout">
-        <LogoutOutlined/>
+        <LogoutOutlined />
       </a>
-    </a-space>
+    </ASpace>
   </div>
 </template>
 
-
 <style lang="less" scoped>
 .header {
   height: 64px;

+ 21 - 20
app/src/layouts/Loading.vue

@@ -1,33 +1,34 @@
+<script>
+export default {
+  name: 'Loading',
+  props: {
+    loading: {
+      type: [Boolean, String],
+      default: false,
+    },
+  },
+}
+</script>
+
 <template>
   <div
     v-show="loading"
     class="loading"
   >
     <div class="wrapper center">
-      <a-spin>
-        <a-icon
-          slot="indicator"
-          spin
-          style="font-size: 30px"
-          type="loading"
-        />
-      </a-spin>
+      <ASpin>
+        <template #indicator>
+          <AIcon
+            spin
+            style="font-size: 30px"
+            type="loading"
+          />
+        </template>
+      </ASpin>
     </div>
   </div>
 </template>
 
-<script>
-export default {
-  name: 'Loading',
-  props: {
-    loading: {
-      type: [Boolean, String],
-      default: false
-    }
-  }
-}
-</script>
-
 <style scoped>
 
 </style>

+ 54 - 44
app/src/layouts/SideBar.vue

@@ -1,108 +1,119 @@
 <script setup lang="ts">
+import { useRoute } from 'vue-router'
+import type { ComputedRef } from 'vue'
+import { computed, ref, watch } from 'vue'
+import type { AntdIconType } from '@ant-design/icons-vue/lib/components/AntdIcon'
 import Logo from '@/components/Logo/Logo.vue'
-import {routes} from '@/routes'
-import {useRoute} from 'vue-router'
-import {computed, ComputedRef, ref, watch} from 'vue'
+import { routes } from '@/routes'
 import EnvIndicator from '@/components/EnvIndicator/EnvIndicator.vue'
 
 const route = useRoute()
 
-let openKeys = [openSub()]
+const openKeys = [openSub()]
 
 const selectedKey = ref([route.name])
 
 function openSub() {
-  let path = route.path
-  let lastSepIndex = path.lastIndexOf('/')
+  const path = route.path
+  const lastSepIndex = path.lastIndexOf('/')
+
   return path.substring(1, lastSepIndex)
 }
 
 watch(route, () => {
   selectedKey.value = [route.name]
+
   const sub = openSub()
   const p = openKeys.indexOf(sub)
-  if (p === -1) openKeys.push(sub)
+  if (p === -1)
+    openKeys.push(sub)
 })
 
 const sidebars = computed(() => {
-  return routes[0]['children']
+  return routes[0].children
 })
 
 interface meta {
-  icon: any
+  icon: AntdIconType
   hiddenInSidebar: boolean
   hideChildren: boolean
 }
 
 interface sidebar {
   path: string
-  name: Function
-  meta: meta,
+  name: () => string
+  meta: meta
   children: sidebar[]
 }
 
 const visible: ComputedRef<sidebar[]> = computed(() => {
-
   const res: sidebar[] = [];
 
-  (sidebars.value || []).forEach((s) => {
-    if (s.meta && s.meta.hiddenInSidebar) {
+  (sidebars.value || []).forEach(s => {
+    if (s.meta && s.meta.hiddenInSidebar)
       return
-    }
+
     const t: sidebar = {
       path: s.path,
       name: s.name,
       meta: s.meta as meta,
-      children: []
+      children: [],
     };
 
-    (s.children || []).forEach((c: any) => {
-      if (c.meta && c.meta.hiddenInSidebar) {
+    (s.children || []).forEach(c => {
+      if (c.meta && c.meta.hiddenInSidebar)
         return
-      }
+
       t.children.push((c as sidebar))
     })
     res.push(t)
   })
 
-
   return res
 })
 </script>
 
 <template>
   <div class="sidebar">
-    <logo/>
+    <Logo />
 
-    <a-menu
-      :openKeys="openKeys"
-      mode="inline"
+    <AMenu
       v-model:openKeys="openKeys"
       v-model:selectedKeys="selectedKey"
+      :open-keys="openKeys"
+      mode="inline"
     >
-      <env-indicator/>
-
-      <template v-for="sidebar in visible">
-        <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren"
-                     :key="sidebar.name"
-                     @click="$router.push('/'+sidebar.path).catch(() => {})">
-          <component :is="sidebar.meta.icon"/>
-          <span>{{ sidebar.name() }}</span>
-        </a-menu-item>
-
-        <a-sub-menu v-else :key="sidebar.path">
+      <EnvIndicator />
+
+      <template v-for="s in visible">
+        <AMenuItem
+          v-if="s.children.length === 0 || s.meta.hideChildren"
+          :key="s.name"
+          @click="$router.push(`/${s.path}`).catch(() => {})"
+        >
+          <component :is="s.meta.icon" />
+          <span>{{ s.name() }}</span>
+        </AMenuItem>
+
+        <ASubMenu
+          v-else
+          :key="s.path"
+        >
           <template #title>
-            <component :is="sidebar.meta.icon"/>
-            <span>{{ sidebar.name() }}</span>
+            <component :is="s.meta.icon" />
+            <span>{{ s.name() }}</span>
           </template>
-          <a-menu-item v-for="child in sidebar.children" :key="child.name">
-            <router-link :to="'/'+sidebar.path+'/'+child.path">
+          <AMenuItem
+            v-for="child in s.children"
+            :key="child.name"
+          >
+            <RouterLink :to="`/${s.path}/${child.path}`">
               {{ child.name() }}
-            </router-link>
-          </a-menu-item>
-        </a-sub-menu>
+            </RouterLink>
+          </AMenuItem>
+        </ASubMenu>
       </template>
-    </a-menu>
+    </AMenu>
   </div>
 </template>
 
@@ -136,7 +147,6 @@ const visible: ComputedRef<sidebar[]> = computed(() => {
       margin-left: 0;
     }
 
-
     .text {
       display: none;
     }

+ 17 - 31
app/src/lib/helper/index.ts

@@ -2,32 +2,37 @@ import dayjs from 'dayjs'
 import relativeTime from 'dayjs/plugin/relativeTime'
 
 function bytesToSize(bytes: number) {
-  if (bytes === 0) return '0 B'
+  if (bytes === 0)
+    return '0 B'
 
   const k = 1024
 
   const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
 
   const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
-}
 
+  return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 function downloadCsv(header: any, data: any[], fileName: string) {
-  if (!header || !Array.isArray(header) || !Array.isArray(data) || !header.length) {
+  if (!header || !Array.isArray(header) || !Array.isArray(data) || !header.length)
     return
-  }
-  let csvContent = 'data:text/csv;charset=utf-8,\ufeff'
+
+  let csvContent = 'data:text/csv;charset=utf-8,\uFEFF'
   const _header = header.map(h => h.title).join(',')
   const keys = header.map(item => item.key)
-  csvContent += _header + '\n'
+
+  csvContent += `${_header}\n`
   data.forEach((item, index) => {
     let dataString = ''
-    for (let i = 0; i < keys.length; i++) {
-      dataString += item[keys[i]] + ','
-    }
+    for (let i = 0; i < keys.length; i++)
+      dataString += `${item[keys[i]]},`
+
     csvContent += index < data.length ? dataString.replace(/,$/, '\n') : dataString.replace(/,$/, '')
   })
+
   const a = document.createElement('a')
+
   a.href = encodeURI(csvContent)
   a.download = fileName
   a.click()
@@ -44,27 +49,9 @@ const urlJoin = (...args: string[]) =>
     .replace(/\?/g, '&')
     .replace('&', '?')
 
-function createEnum(definition: any) {
-  const strToValueMap: any = {}
-  const numToDescMap: any = {}
-  for (const enumName of Object.keys(definition)) {
-    const [value, desc] = definition[enumName]
-    strToValueMap[enumName] = value
-    numToDescMap[value] = desc
-  }
-  return {
-    ...strToValueMap,
-    getDesc(enumName: any) {
-      return (definition[enumName] && definition[enumName][1]) || ''
-    },
-    getDescFromValue(value: any) {
-      return numToDescMap[value] || ''
-    }
-  }
-}
-
 function fromNow(t: string) {
   dayjs.extend(relativeTime)
+
   return dayjs(t).fromNow()
 }
 
@@ -80,8 +67,7 @@ export {
   bytesToSize,
   downloadCsv,
   urlJoin,
-  createEnum,
   fromNow,
   formatDate,
-  formatDateTime
+  formatDateTime,
 }

+ 26 - 15
app/src/lib/http/index.ts

@@ -1,49 +1,54 @@
-import axios, {AxiosRequestConfig} from 'axios'
-import {useSettingsStore, useUserStore} from '@/pinia'
-import {storeToRefs} from 'pinia'
+import type { AxiosRequestConfig } from 'axios'
+import axios from 'axios'
+import { storeToRefs } from 'pinia'
 import NProgress from 'nprogress'
+import { useSettingsStore, useUserStore } from '@/pinia'
 import 'nprogress/nprogress.css'
 
 import router from '@/routes'
 
 const user = useUserStore()
 const settings = useSettingsStore()
-const {token} = storeToRefs(user)
+const { token } = storeToRefs(user)
 
-let instance = axios.create({
+const instance = axios.create({
   baseURL: import.meta.env.VITE_API_ROOT,
   timeout: 50000,
-  headers: {'Content-Type': 'application/json'},
+  headers: { 'Content-Type': 'application/json' },
   transformRequest: [function (data, headers) {
-    if (!(headers) || headers['Content-Type'] === 'multipart/form-data;charset=UTF-8') {
+    if (!(headers) || headers['Content-Type'] === 'multipart/form-data;charset=UTF-8')
       return data
-    } else {
+    else
       headers['Content-Type'] = 'application/json'
-    }
+
     return JSON.stringify(data)
-  }]
+  }],
 })
 
-
 instance.interceptors.request.use(
   config => {
     NProgress.start()
     if (token) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
       (config.headers as any).Authorization = token.value
     }
+
     if (settings.environment.id) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
       (config.headers as any)['X-Node-ID'] = settings.environment.id
     }
+
     return config
   },
   err => {
     return Promise.reject(err)
-  }
+  },
 )
 
 instance.interceptors.response.use(
   response => {
     NProgress.done()
+
     return Promise.resolve(response.data)
   },
   async error => {
@@ -55,24 +60,30 @@ instance.interceptors.response.use(
         await router.push('/login')
         break
     }
+
     return Promise.reject(error.response.data)
-  }
+  },
 )
 
 const http = {
   get(url: string, config: AxiosRequestConfig = {}) {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     return instance.get<any, any>(url, config)
   },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   post(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     return instance.post<any, any>(url, data, config)
   },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   put(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     return instance.put<any, any>(url, data, config)
   },
   delete(url: string, config: AxiosRequestConfig = {}) {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     return instance.delete<any, any>(url, config)
-  }
+  },
 }
 
-
 export default http

+ 7 - 9
app/src/lib/websocket/index.ts

@@ -1,24 +1,22 @@
 import ReconnectingWebSocket from 'reconnecting-websocket'
-import {useSettingsStore, useUserStore} from '@/pinia'
-import {storeToRefs} from 'pinia'
-import {urlJoin} from '@/lib/helper'
-
+import { storeToRefs } from 'pinia'
+import { useSettingsStore, useUserStore } from '@/pinia'
+import { urlJoin } from '@/lib/helper'
 
 function ws(url: string, reconnect: boolean = true): ReconnectingWebSocket | WebSocket {
   const user = useUserStore()
   const settings = useSettingsStore()
-  const {token} = storeToRefs(user)
+  const { token } = storeToRefs(user)
 
   const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'
 
-  const node_id = (settings.environment.id > 0) ? ('&x_node_id=' + settings.environment.id) : ''
+  const node_id = (settings.environment.id > 0) ? (`&x_node_id=${settings.environment.id}`) : ''
 
   const _url = urlJoin(protocol + window.location.host, window.location.pathname,
-    url, '?token=' + btoa(token.value), node_id)
+    url, `?token=${btoa(token.value)}`, node_id)
 
-  if (reconnect) {
+  if (reconnect)
     return new ReconnectingWebSocket(_url)
-  }
 
   return new WebSocket(_url)
 }

+ 7 - 5
app/src/main.ts

@@ -1,11 +1,11 @@
-import {createApp} from 'vue'
-import {createPinia} from 'pinia'
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
 import gettext from './gettext'
 import App from './App.vue'
 import router from './routes'
-import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
-import {useSettingsStore} from '@/pinia'
-import {autoAnimatePlugin} from '@formkit/auto-animate/vue'
+import { useSettingsStore } from '@/pinia'
 import './style.css'
 
 const pinia = createPinia()
@@ -15,8 +15,10 @@ const app = createApp(App)
 pinia.use(piniaPluginPersistedstate)
 app.use(pinia)
 app.use(gettext)
+
 // after pinia created
 const settings = useSettingsStore()
+
 gettext.current = settings.language || 'en'
 
 app.use(router).use(autoAnimatePlugin).mount('#app')

+ 3 - 3
app/src/pinia/index.ts

@@ -1,7 +1,7 @@
-import {useUserStore} from './moudule/user'
-import {useSettingsStore} from './moudule/settings'
+import { useUserStore } from './moudule/user'
+import { useSettingsStore } from './moudule/settings'
 
 export {
   useUserStore,
-  useSettingsStore
+  useSettingsStore,
 }

+ 7 - 7
app/src/pinia/moudule/settings.ts

@@ -1,4 +1,4 @@
-import {defineStore} from 'pinia'
+import { defineStore } from 'pinia'
 
 export const useSettingsStore = defineStore('settings', {
   state: () => ({
@@ -7,13 +7,13 @@ export const useSettingsStore = defineStore('settings', {
     preference_theme: 'auto',
     environment: {
       id: 0,
-      name: 'Local'
-    }
+      name: 'Local',
+    },
   }),
   getters: {
     is_remote(): boolean {
       return this.environment.id !== 0
-    }
+    },
   },
   actions: {
     set_language(lang: string) {
@@ -21,7 +21,7 @@ export const useSettingsStore = defineStore('settings', {
     },
     set_theme(t: string) {
       this.theme = t
-      document.body.setAttribute('class', t == 'dark' ? 'dark' : 'light')
+      document.body.setAttribute('class', t === 'dark' ? 'dark' : 'light')
     },
     set_preference_theme(t: string) {
       this.preference_theme = t
@@ -29,7 +29,7 @@ export const useSettingsStore = defineStore('settings', {
     clear_environment() {
       this.environment.id = 0
       this.environment.name = 'Local'
-    }
+    },
   },
-  persist: true
+  persist: true,
 })

+ 5 - 5
app/src/pinia/moudule/user.ts

@@ -1,13 +1,13 @@
-import {defineStore} from 'pinia'
+import { defineStore } from 'pinia'
 
 export const useUserStore = defineStore('user', {
   state: () => ({
-    token: ''
+    token: '',
   }),
   getters: {
     is_login(state): boolean {
       return !!state.token
-    }
+    },
   },
   actions: {
     login(token: string) {
@@ -15,7 +15,7 @@ export const useUserStore = defineStore('user', {
     },
     logout() {
       this.token = ''
-    }
+    },
   },
-  persist: true
+  persist: true,
 })

+ 77 - 59
app/src/routes/index.ts

@@ -1,6 +1,4 @@
-import {createRouter, createWebHashHistory} from 'vue-router'
-import gettext from '../gettext'
-import {useUserStore} from '@/pinia'
+import { createRouter, createWebHashHistory } from 'vue-router'
 
 import {
   CloudOutlined,
@@ -12,14 +10,35 @@ import {
   InfoCircleOutlined,
   SafetyCertificateOutlined,
   SettingOutlined,
-  UserOutlined
+  UserOutlined,
 } from '@ant-design/icons-vue'
 import NProgress from 'nprogress'
+import type { AntDesignOutlinedIconType } from '@ant-design/icons-vue/lib/icons/AntDesignOutlined'
+
+import gettext from '../gettext'
+import { useUserStore } from '@/pinia'
+
 import 'nprogress/nprogress.css'
 
-const {$gettext} = gettext
+const { $gettext } = gettext
+
+export interface Route {
+  path: string
+  name: () => string
+  component?: () => Promise<typeof import('*.vue')>
+  redirect?: string
+  meta?: {
+    icon?: AntDesignOutlinedIconType
+    hiddenInSidebar?: boolean
+    hideChildren?: boolean
+    noAuth?: boolean
+    status_code?: number
+    error?: () => string
+  }
+  children?: Route[]
+}
 
-export const routes = [
+export const routes: Route[] = [
   {
     path: '/',
     name: () => $gettext('Home'),
@@ -32,33 +51,33 @@ export const routes = [
         name: () => $gettext('Dashboard'),
         meta: {
           // hiddenHeaderContent: true,
-          icon: HomeOutlined
-        }
+          icon: HomeOutlined,
+        },
       },
       {
         path: 'domain',
         name: () => $gettext('Manage Sites'),
         component: () => import('@/layouts/BaseRouterView.vue'),
         meta: {
-          icon: CloudOutlined
+          icon: CloudOutlined,
         },
         redirect: '/domain/list',
         children: [{
           path: 'list',
           name: () => $gettext('Sites List'),
-          component: () => import('@/views/domain/DomainList.vue')
+          component: () => import('@/views/domain/DomainList.vue'),
         }, {
           path: 'add',
           name: () => $gettext('Add Site'),
-          component: () => import('@/views/domain/DomainAdd.vue')
+          component: () => import('@/views/domain/DomainAdd.vue'),
         }, {
           path: ':name',
           name: () => $gettext('Edit Site'),
           component: () => import('@/views/domain/DomainEdit.vue'),
           meta: {
-            hiddenInSidebar: true
-          }
-        }]
+            hiddenInSidebar: true,
+          },
+        }],
       },
       {
         path: 'config',
@@ -66,154 +85,153 @@ export const routes = [
         component: () => import('@/views/config/Config.vue'),
         meta: {
           icon: FileOutlined,
-          hideChildren: true
-        }
+          hideChildren: true,
+        },
       },
       {
         path: 'config/:name+/edit',
         name: () => $gettext('Edit Configuration'),
         component: () => import('@/views/config/ConfigEdit.vue'),
         meta: {
-          hiddenInSidebar: true
-        }
+          hiddenInSidebar: true,
+        },
       },
       {
         path: 'cert',
         name: () => $gettext('Certification'),
         component: () => import('@/layouts/BaseRouterView.vue'),
         meta: {
-          icon: SafetyCertificateOutlined
+          icon: SafetyCertificateOutlined,
         },
         children: [
           {
             path: 'list',
             name: () => $gettext('Certification List'),
-            component: () => import('@/views/cert/Cert.vue')
+            component: () => import('@/views/cert/Cert.vue'),
           },
           {
             path: 'dns_credential',
             name: () => $gettext('DNS Credentials'),
-            component: () => import('@/views/cert/DNSCredential.vue')
-          }
-        ]
+            component: () => import('@/views/cert/DNSCredential.vue'),
+          },
+        ],
       },
       {
         path: 'terminal',
         name: () => $gettext('Terminal'),
         component: () => import('@/views/pty/Terminal.vue'),
         meta: {
-          icon: CodeOutlined
-        }
+          icon: CodeOutlined,
+        },
       },
       {
         path: 'nginx_log',
         name: () => $gettext('Nginx Log'),
         meta: {
-          icon: FileTextOutlined
+          icon: FileTextOutlined,
         },
         children: [{
           path: 'access',
           name: () => $gettext('Access Logs'),
-          component: () => import('@/views/nginx_log/NginxLog.vue')
+          component: () => import('@/views/nginx_log/NginxLog.vue'),
         }, {
           path: 'error',
           name: () => $gettext('Error Logs'),
-          component: () => import('@/views/nginx_log/NginxLog.vue')
+          component: () => import('@/views/nginx_log/NginxLog.vue'),
         }, {
           path: 'site',
           name: () => $gettext('Site Logs'),
           component: () => import('@/views/nginx_log/NginxLog.vue'),
           meta: {
-            hiddenInSidebar: true
-          }
-        }]
+            hiddenInSidebar: true,
+          },
+        }],
       },
       {
         path: 'environment',
         name: () => $gettext('Environment'),
         component: () => import('@/views/environment/Environment.vue'),
         meta: {
-          icon: DatabaseOutlined
-        }
+          icon: DatabaseOutlined,
+        },
       },
       {
         path: 'user',
         name: () => $gettext('Manage Users'),
         component: () => import('@/views/user/User.vue'),
         meta: {
-          icon: UserOutlined
-        }
+          icon: UserOutlined,
+        },
       },
       {
         path: 'preference',
         name: () => $gettext('Preference'),
         component: () => import('@/views/preference/Preference.vue'),
         meta: {
-          icon: SettingOutlined
-        }
+          icon: SettingOutlined,
+        },
       },
       {
         path: 'system',
         name: () => $gettext('System'),
         redirect: 'system/about',
         meta: {
-          icon: InfoCircleOutlined
+          icon: InfoCircleOutlined,
         },
         children: [{
           path: 'about',
           name: () => $gettext('About'),
-          component: () => import('@/views/system/About.vue')
+          component: () => import('@/views/system/About.vue'),
         }, {
           path: 'upgrade',
           name: () => $gettext('Upgrade'),
-          component: () => import('@/views/system/Upgrade.vue')
-        }]
-      }
-    ]
+          component: () => import('@/views/system/Upgrade.vue'),
+        }],
+      },
+    ],
   },
   {
     path: '/install',
     name: () => $gettext('Install'),
     component: () => import('@/views/other/Install.vue'),
-    meta: {noAuth: true}
+    meta: { noAuth: true },
   },
   {
     path: '/login',
     name: () => $gettext('Login'),
     component: () => import('@/views/other/Login.vue'),
-    meta: {noAuth: true}
+    meta: { noAuth: true },
   },
   {
     path: '/:pathMatch(.*)*',
     name: () => $gettext('Not Found'),
     component: () => import('@/views/other/Error.vue'),
-    meta: {noAuth: true, status_code: 404, error: () => $gettext('Not Found')}
-  }
+    meta: { noAuth: true, status_code: 404, error: () => $gettext('Not Found') },
+  },
 ]
 
 const router = createRouter({
   history: createWebHashHistory(),
-  // @ts-ignore
-  routes: routes
+
+  // @ts-expect-error routes type error
+  routes,
 })
 
-NProgress.configure({showSpinner: false})
+NProgress.configure({ showSpinner: false })
 
-router.beforeEach((to, from, next) => {
-  // @ts-ignore
-  document.title = to.name?.() + ' | Nginx UI'
+router.beforeEach((to, _, next) => {
+  // @ts-expect-error name type
+  document.title = `${to.name?.()} | Nginx UI`
 
   NProgress.start()
 
   const user = useUserStore()
-  const {is_login} = user
+  const { is_login } = user
 
-  if (to.meta.noAuth || is_login) {
+  if (to.meta.noAuth || is_login)
     next()
-  } else {
-    next({path: '/login', query: {next: to.fullPath}})
-  }
-
+  else
+    next({ path: '/login', query: { next: to.fullPath } })
 })
 
 router.afterEach(() => {

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.4","build_id":50,"total_build":254}
+{"version":"2.0.0-beta.4","build_id":53,"total_build":257}

Некоторые файлы не были показаны из-за большого количества измененных файлов