Browse Source

refactor: refresh webui

Jacky 1 year ago
parent
commit
4c7e037b76
100 changed files with 1550 additions and 857 deletions
  1. 96 0
      api/certificate/acme_user.go
  2. 1 1
      api/certificate/certificate.go
  3. 1 1
      api/certificate/dns_credential.go
  4. 10 0
      api/certificate/router.go
  5. 1 1
      api/notification/notification.go
  6. 1 1
      api/user/user.go
  7. 4 4
      app/.eslintrc.cjs
  8. 12 0
      app/auto-imports.d.ts
  9. 3 0
      app/components.d.ts
  10. 3 3
      app/package.json
  11. 205 205
      app/pnpm-lock.yaml
  12. 2 1
      app/src/App.vue
  13. 24 0
      app/src/api/acme_user.ts
  14. 8 2
      app/src/api/curd.ts
  15. 2 2
      app/src/components/Breadcrumb/Breadcrumb.vue
  16. 1 4
      app/src/components/ChatGPT/ChatGPT.vue
  17. 0 2
      app/src/components/EnvIndicator/EnvIndicator.vue
  18. 0 3
      app/src/components/NginxControl/NginxControl.vue
  19. 0 3
      app/src/components/NodeSelector/NodeSelector.vue
  20. 0 2
      app/src/components/Notification/Notification.vue
  21. 1 1
      app/src/components/PageHeader/PageHeader.vue
  22. 3 4
      app/src/components/SetLanguage/SetLanguage.vue
  23. 0 3
      app/src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue
  24. 184 43
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  25. 36 0
      app/src/components/StdDesign/StdDataDisplay/StdCurdDetail.vue
  26. 3 2
      app/src/components/StdDesign/StdDataDisplay/StdPagination.vue
  27. 270 105
      app/src/components/StdDesign/StdDataDisplay/StdTable.vue
  28. 64 10
      app/src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx
  29. 8 0
      app/src/components/StdDesign/StdDataDisplay/components/CustomRender.tsx
  30. 18 18
      app/src/components/StdDesign/StdDataDisplay/methods/exportCsv.ts
  31. 12 12
      app/src/components/StdDesign/StdDataDisplay/methods/sortable.ts
  32. 48 15
      app/src/components/StdDesign/StdDataEntry/StdDataEntry.vue
  33. 9 18
      app/src/components/StdDesign/StdDataEntry/StdFormItem.vue
  34. 0 2
      app/src/components/StdDesign/StdDataEntry/components/StdPassword.vue
  35. 52 29
      app/src/components/StdDesign/StdDataEntry/components/StdSelect.vue
  36. 1 2
      app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue
  37. 109 51
      app/src/components/StdDesign/StdDataEntry/index.tsx
  38. 58 40
      app/src/components/StdDesign/types.d.ts
  39. 0 3
      app/src/components/SwitchAppearance/SwitchAppearance.vue
  40. 1 2
      app/src/constants/index.ts
  41. 4 2
      app/src/gettext.ts
  42. 0 4
      app/src/language/constants.ts
  43. 0 3
      app/src/layouts/HeaderLayout.vue
  44. 9 7
      app/src/layouts/SideBar.vue
  45. 4 0
      app/src/lib/http/index.ts
  46. 89 58
      app/src/routes/index.ts
  47. 20 0
      app/src/routes/type.d.ts
  48. 1 0
      app/src/types.d.ts
  49. 1 1
      app/src/version.json
  50. 109 0
      app/src/views/certificate/ACMEUser.vue
  51. 5 9
      app/src/views/certificate/Certificate.vue
  52. 0 3
      app/src/views/certificate/CertificateEditor.vue
  53. 0 2
      app/src/views/certificate/DNSChallenge.vue
  54. 0 3
      app/src/views/certificate/DNSCredential.vue
  55. 0 3
      app/src/views/certificate/RenewCert.vue
  56. 0 2
      app/src/views/certificate/WildcardCertificate.vue
  57. 0 3
      app/src/views/config/Config.vue
  58. 3 4
      app/src/views/config/ConfigEdit.vue
  59. 0 3
      app/src/views/config/InspectConfig.vue
  60. 2 4
      app/src/views/config/config.ts
  61. 1 4
      app/src/views/dashboard/Environments.vue
  62. 0 3
      app/src/views/dashboard/ServerAnalytic.vue
  63. 2 2
      app/src/views/dashboard/components/NodeAnalyticItem.vue
  64. 0 3
      app/src/views/domain/DomainAdd.vue
  65. 5 8
      app/src/views/domain/DomainEdit.vue
  66. 2 5
      app/src/views/domain/DomainList.vue
  67. 0 2
      app/src/views/domain/cert/Cert.vue
  68. 0 3
      app/src/views/domain/cert/CertInfo.vue
  69. 2 5
      app/src/views/domain/cert/ChangeCert.vue
  70. 0 2
      app/src/views/domain/cert/IssueCert.vue
  71. 0 3
      app/src/views/domain/cert/components/AutoCertStepOne.vue
  72. 3 4
      app/src/views/domain/cert/components/DNSChallenge.vue
  73. 4 7
      app/src/views/domain/cert/components/ObtainCert.vue
  74. 1 4
      app/src/views/domain/cert/components/ObtainCertLive.vue
  75. 0 3
      app/src/views/domain/components/Deploy.vue
  76. 4 4
      app/src/views/domain/components/RightSettings.vue
  77. 3 5
      app/src/views/domain/components/SiteDuplicate.vue
  78. 0 3
      app/src/views/domain/ngx_conf/LocationEditor.vue
  79. 0 2
      app/src/views/domain/ngx_conf/LogEntry.vue
  80. 3 5
      app/src/views/domain/ngx_conf/NgxConfigEditor.vue
  81. 0 3
      app/src/views/domain/ngx_conf/NgxServer.vue
  82. 0 3
      app/src/views/domain/ngx_conf/NgxUpstream.vue
  83. 0 2
      app/src/views/domain/ngx_conf/config_template/ConfigTemplate.vue
  84. 0 3
      app/src/views/domain/ngx_conf/directive/DirectiveAdd.vue
  85. 0 2
      app/src/views/domain/ngx_conf/directive/DirectiveEditor.vue
  86. 1 4
      app/src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue
  87. 3 6
      app/src/views/environment/Environment.vue
  88. 0 2
      app/src/views/nginx_log/NginxLog.vue
  89. 1 4
      app/src/views/notification/Notification.vue
  90. 0 3
      app/src/views/other/Error.vue
  91. 5 7
      app/src/views/other/Install.vue
  92. 2 2
      app/src/views/other/Login.vue
  93. 0 2
      app/src/views/preference/BasicSettings.vue
  94. 0 3
      app/src/views/preference/LogrotateSettings.vue
  95. 0 3
      app/src/views/preference/NginxSettings.vue
  96. 0 3
      app/src/views/preference/OpenAISettings.vue
  97. 0 3
      app/src/views/preference/Preference.vue
  98. 3 7
      app/src/views/pty/Terminal.vue
  99. 5 7
      app/src/views/stream/StreamEdit.vue
  100. 2 5
      app/src/views/stream/StreamList.vue

+ 96 - 0
api/certificate/acme_user.go

@@ -0,0 +1,96 @@
+package certificate
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
+	"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/spf13/cast"
+	"net/http"
+)
+
+func GetAcmeUser(c *gin.Context) {
+	u := query.AcmeUser
+	id := cast.ToInt(c.Param("id"))
+	user, err := u.FirstByID(id)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, user)
+}
+
+func CreateAcmeUser(c *gin.Context) {
+	cosy.Core[model.AcmeUser](c).SetValidRules(gin.H{
+		"name":   "required",
+		"email":  "required,email",
+		"ca_dir": "omitempty",
+	}).BeforeExecuteHook(func(ctx *cosy.Ctx[model.AcmeUser]) {
+		if ctx.Model.CADir == "" {
+			ctx.Model.CADir = settings.ServerSettings.CADir
+		}
+		err := ctx.Model.Register()
+		if err != nil {
+			ctx.AbortWithError(err)
+			return
+		}
+	}).Create()
+}
+
+func ModifyAcmeUser(c *gin.Context) {
+	cosy.Core[model.AcmeUser](c).SetValidRules(gin.H{
+		"name":   "omitempty",
+		"email":  "omitempty,email",
+		"ca_dir": "omitempty",
+	}).BeforeExecuteHook(func(ctx *cosy.Ctx[model.AcmeUser]) {
+		if ctx.Model.CADir == "" {
+			ctx.Model.CADir = settings.ServerSettings.CADir
+		}
+
+		if ctx.OriginModel.Email != ctx.Model.Email ||
+			ctx.OriginModel.CADir != ctx.Model.CADir {
+			err := ctx.Model.Register()
+			if err != nil {
+				ctx.AbortWithError(err)
+				return
+			}
+		}
+	}).Modify()
+}
+
+func GetAcmeUserList(c *gin.Context) {
+	cosy.Core[model.AcmeUser](c).
+		SetFussy("name", "email").
+		PagingList()
+}
+
+func DestroyAcmeUser(c *gin.Context) {
+	cosy.Core[model.AcmeUser](c).Destroy()
+}
+
+func RecoverAcmeUser(c *gin.Context) {
+	cosy.Core[model.AcmeUser](c).Recover()
+}
+
+func RegisterAcmeUser(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+	u := query.AcmeUser
+	user, err := u.FirstByID(id)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	err = user.Register()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	_, err = u.Where(u.ID.Eq(id)).Updates(user)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, user)
+}

+ 1 - 1
api/certificate/certificate.go

@@ -2,8 +2,8 @@ package certificate
 
 import (
 	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/api/cosy"
 	"github.com/0xJacky/Nginx-UI/internal/cert"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"

+ 1 - 1
api/certificate/dns_credential.go

@@ -2,8 +2,8 @@ package certificate
 
 import (
 	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/api/cosy"
 	"github.com/0xJacky/Nginx-UI/internal/cert/dns"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"

+ 10 - 0
api/certificate/router.go

@@ -23,3 +23,13 @@ func InitCertificateRouter(r *gin.RouterGroup) {
 func InitCertificateWebSocketRouter(r *gin.RouterGroup) {
 	r.GET("domain/:name/cert", IssueCert)
 }
+
+func InitAcmeUserRouter(r *gin.RouterGroup) {
+	r.GET("acme_users", GetAcmeUserList)
+	r.GET("acme_user/:id", GetAcmeUser)
+	r.POST("acme_user", CreateAcmeUser)
+	r.POST("acme_user/:id", ModifyAcmeUser)
+	r.POST("acme_user/:id/register", RegisterAcmeUser)
+	r.DELETE("acme_user/:id", DestroyAcmeUser)
+	r.PATCH("acme_user/:id", RecoverAcmeUser)
+}

+ 1 - 1
api/notification/notification.go

@@ -2,7 +2,7 @@ package notification
 
 import (
 	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/api/cosy"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"

+ 1 - 1
api/user/user.go

@@ -2,7 +2,7 @@ package user
 
 import (
 	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/api/cosy"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/0xJacky/Nginx-UI/settings"

+ 4 - 4
app/.eslintrc.cjs

@@ -40,7 +40,7 @@ module.exports = {
       before: false,
       after: true,
     }],
-    'key-spacing': ['error', { afterColon: true }],
+    'key-spacing': ['error', {afterColon: true}],
 
     'vue/first-attribute-linebreak': ['error', {
       singleline: 'beside',
@@ -78,7 +78,7 @@ module.exports = {
     ],
 
     // Ignore _ as unused variable
-    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_+$' }],
+    '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_+$'}],
 
     'array-element-newline': ['error', 'consistent'],
     'array-bracket-newline': ['error', 'consistent'],
@@ -111,7 +111,7 @@ module.exports = {
 
     // Plugin: eslint-plugin-import
     'import/prefer-default-export': 'off',
-    'import/newline-after-import': ['error', { count: 1 }],
+    'import/newline-after-import': ['error', {count: 1}],
     'no-restricted-imports': ['error', 'vuetify/components'],
 
     // For omitting extension for ts files
@@ -150,7 +150,7 @@ module.exports = {
 
     // ESLint plugin vue
     'vue/component-api-style': 'error',
-    'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }],
+    '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])?/',

+ 12 - 0
app/auto-imports.d.ts

@@ -5,6 +5,10 @@
 // Generated by unplugin-auto-import
 export {}
 declare global {
+  const $gettext: typeof import('@/gettext')['$gettext']
+  const $ngettext: typeof import('@/gettext')['$ngettext']
+  const $npgettext: typeof import('@/gettext')['$npgettext']
+  const $pgettext: typeof import('@/gettext')['$pgettext']
   const EffectScope: typeof import('vue')['EffectScope']
   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
   const computed: typeof import('vue')['computed']
@@ -86,6 +90,10 @@ import { UnwrapRef } from 'vue'
 declare module 'vue' {
   interface GlobalComponents {}
   interface ComponentCustomProperties {
+    readonly $gettext: UnwrapRef<typeof import('@/gettext')['$gettext']>
+    readonly $ngettext: UnwrapRef<typeof import('@/gettext')['$ngettext']>
+    readonly $npgettext: UnwrapRef<typeof import('@/gettext')['$npgettext']>
+    readonly $pgettext: UnwrapRef<typeof import('@/gettext')['$pgettext']>
     readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
     readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
     readonly computed: UnwrapRef<typeof import('vue')['computed']>
@@ -160,6 +168,10 @@ declare module 'vue' {
 declare module '@vue/runtime-core' {
   interface GlobalComponents {}
   interface ComponentCustomProperties {
+    readonly $gettext: UnwrapRef<typeof import('@/gettext')['$gettext']>
+    readonly $ngettext: UnwrapRef<typeof import('@/gettext')['$ngettext']>
+    readonly $npgettext: UnwrapRef<typeof import('@/gettext')['$npgettext']>
+    readonly $pgettext: UnwrapRef<typeof import('@/gettext')['$pgettext']>
     readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
     readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
     readonly computed: UnwrapRef<typeof import('vue')['computed']>

+ 3 - 0
app/components.d.ts

@@ -21,6 +21,8 @@ declare module 'vue' {
     ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
     AComment: typeof import('ant-design-vue/es')['Comment']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
+    ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
     ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
     ADropdown: typeof import('ant-design-vue/es')['Dropdown']
@@ -80,6 +82,7 @@ declare module 'vue' {
     SetLanguageSetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
     StdDesignStdDataDisplayStdBatchEdit: typeof import('./src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue')['default']
     StdDesignStdDataDisplayStdCurd: typeof import('./src/components/StdDesign/StdDataDisplay/StdCurd.vue')['default']
+    StdDesignStdDataDisplayStdCurdDetail: typeof import('./src/components/StdDesign/StdDataDisplay/StdCurdDetail.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']

+ 3 - 3
app/package.json

@@ -29,14 +29,14 @@
     "reconnecting-websocket": "^4.4.0",
     "sortablejs": "^1.15.2",
     "vite-plugin-build-id": "^0.2.8",
-    "vue": "^3.4.25",
+    "vue": "^3.4.26",
     "vue-github-button": "github:0xJacky/vue-github-button",
     "vue-router": "^4.3.2",
     "vue3-ace-editor": "2.2.4",
     "vue3-apexcharts": "1.4.4",
     "vue3-gettext": "3.0.0-beta.4",
     "vuedraggable": "^4.1.0",
-    "xterm": "^5.3.0",
+    "@xterm/xterm": "^5.5.0",
     "@xterm/addon-attach": "^0.11.0",
     "@xterm/addon-fit": "^0.10.0"
   },
@@ -63,7 +63,7 @@
     "less": "^4.2.0",
     "postcss": "^8.4.38",
     "tailwindcss": "^3.4.3",
-    "typescript": "^5.4.5",
+    "typescript": "5.3.3",
     "unplugin-auto-import": "^0.17.5",
     "unplugin-vue-components": "^0.26.0",
     "unplugin-vue-define-options": "^1.4.3",

File diff suppressed because it is too large
+ 205 - 205
app/pnpm-lock.yaml


+ 2 - 1
app/src/App.vue

@@ -7,8 +7,9 @@ import { theme } from 'ant-design-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 gettext from '@/gettext'
+
 import { useSettingsStore } from '@/pinia'
+import gettext from '@/gettext'
 
 const media = window.matchMedia('(prefers-color-scheme: dark)')
 

+ 24 - 0
app/src/api/acme_user.ts

@@ -0,0 +1,24 @@
+import type { ModelBase } from '@/api/curd'
+import Curd from '@/api/curd'
+import http from '@/lib/http'
+
+export interface AcmeUser extends ModelBase {
+  name: string
+  email: string
+  ca_dir: string
+  registration: { body?: { status: string } }
+}
+
+class ACMEUserCurd extends Curd<AcmeUser> {
+  constructor() {
+    super('acme_user', 'acme_users')
+  }
+
+  public async register(id: number) {
+    return http.post(`${this.baseUrl}/${id}/register`)
+  }
+}
+
+const acme_user = new ACMEUserCurd()
+
+export default acme_user

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

@@ -26,6 +26,7 @@ class Curd<T> {
   get = this._get.bind(this)
   save = this._save.bind(this)
   destroy = this._destroy.bind(this)
+  recover = this._recover.bind(this)
   update_order = this._update_order.bind(this)
 
   constructor(baseUrl: string, plural: string | null = null) {
@@ -39,8 +40,8 @@ class Curd<T> {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  _get(id: any = null): Promise<T> {
-    return http.get(this.baseUrl + (id ? `/${id}` : ''))
+  _get(id: any = null, params: any = {}): Promise<T> {
+    return http.get(this.baseUrl + (id ? `/${id}` : ''), { params })
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -53,6 +54,11 @@ class Curd<T> {
     return http.delete(`${this.baseUrl}/${id}`)
   }
 
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  _recover(id: any = null) {
+    return http.patch(`${this.baseUrl}/${id}`)
+  }
+
   _update_order(data: {
     target_id: number
     direction: number

+ 2 - 2
app/src/components/Breadcrumb/Breadcrumb.vue

@@ -12,12 +12,12 @@ const route = useRoute()
 const breadList = computed(() => {
   const _breadList: bread[] = []
 
-  name.value = route.name
+  name.value = route.meta.name
 
   route.matched.forEach(item => {
     // item.name !== 'index' && this.breadList.push(item)
     _breadList.push({
-      name: item.name as never as () => string,
+      name: item.meta.name as never as () => string,
       path: item.path,
     })
   })

+ 1 - 4
app/src/components/ChatGPT/ChatGPT.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 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'
@@ -11,7 +10,7 @@ import 'highlight.js/styles/vs2015.css'
 
 import type { ChatComplicationMessage } from '@/api/openai'
 import openai from '@/api/openai'
-import ChatGPT_logo from '@/assets/svg/ChatGPT_logo.svg'
+import ChatGPT_logo from '@/assets/svg/ChatGPT_logo.svg?component'
 
 const props = defineProps<{
   content: string
@@ -21,8 +20,6 @@ const props = defineProps<{
 
 const emit = defineEmits(['update:history_messages'])
 
-const { $gettext } = useGettext()
-
 const { language: current } = storeToRefs(useSettingsStore())
 
 const history_messages = computed(() => props.historyMessages)

+ 0 - 2
app/src/components/EnvIndicator/EnvIndicator.vue

@@ -1,12 +1,10 @@
 <script setup lang="ts">
-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 settingsStore = useSettingsStore()
 
 const { environment } = storeToRefs(settingsStore)

+ 0 - 3
app/src/components/NginxControl/NginxControl.vue

@@ -1,13 +1,10 @@
 <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 { NginxStatus } from '@/constants'
 
-const { $gettext } = gettext
-
 const status = ref(0)
 function get_status() {
   ngx.status().then(r => {

+ 0 - 3
app/src/components/NodeSelector/NodeSelector.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type { Ref } from 'vue'
 import type { Environment } from '@/api/environment'
 import environment from '@/api/environment'
@@ -12,8 +11,6 @@ const props = defineProps<{
 
 const emit = defineEmits(['update:target', 'update:map'])
 
-const { $gettext } = useGettext()
-
 const data = ref([]) as Ref<Environment[]>
 const data_map = ref({}) as Ref<Record<number, Environment>>
 

+ 0 - 2
app/src/components/Notification/Notification.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { BellOutlined, CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, InfoCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
-import { useGettext } from 'vue3-gettext'
 import type { Ref } from 'vue'
 import { message } from 'ant-design-vue'
 import notification from '@/api/notification'
@@ -8,7 +7,6 @@ import type { Notification } from '@/api/notification'
 import { NotificationTypeT } from '@/constants'
 import { useUserStore } from '@/pinia'
 
-const { $gettext } = useGettext()
 const loading = ref(false)
 
 const { unreadCount } = storeToRefs(useUserStore())

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

@@ -9,7 +9,7 @@ const display = computed(() => {
 })
 
 const name = computed(() => {
-  return (route.name as never as () => string)()
+  return (route.meta.name as never as () => string)()
 })
 </script>
 

+ 3 - 4
app/src/components/SetLanguage/SetLanguage.vue

@@ -1,9 +1,9 @@
 <script setup lang="ts">
 import { ref, watch } from 'vue'
-import gettext from '@/gettext'
 
 import { useSettingsStore } from '@/pinia'
 import http from '@/lib/http'
+import gettext from '@/gettext'
 
 const settings = useSettingsStore()
 
@@ -19,8 +19,7 @@ async function init() {
       gettext.translations[current.value] = r
     })
 
-    // @ts-expect-error name type
-    document.title = `${route.name?.()} | Nginx UI`
+    document.title = `${route.meta.name?.()} | Nginx UI`
   }
 }
 
@@ -31,7 +30,7 @@ watch(current, v => {
   settings.set_language(v)
   gettext.current = v
 
-  const name = route.name as never as () => string
+  const name = route.meta.name as never as () => string
 
   document.title = `${name()} | Nginx UI`
 })

+ 0 - 3
app/src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { message } from 'ant-design-vue'
-import gettext from '@/gettext'
 import StdDataEntry from '@/components/StdDesign/StdDataEntry'
 
 const props = defineProps<{
@@ -11,8 +10,6 @@ const props = defineProps<{
 
 const emit = defineEmits(['onSave'])
 
-const { $gettext } = gettext
-
 const batchColumns = ref([])
 
 const visible = ref(false)

+ 184 - 43
app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -3,13 +3,17 @@ import { message } from 'ant-design-vue'
 import type { ComputedRef } from 'vue'
 import type { StdTableProps } from './StdTable.vue'
 import StdTable from './StdTable.vue'
-import gettext from '@/gettext'
 import StdDataEntry from '@/components/StdDesign/StdDataEntry'
 import type { Column } from '@/components/StdDesign/types'
+import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue'
 
 export interface StdCurdProps {
   cardTitleKey?: string
   modalMaxWidth?: string | number
+  modalMask?: boolean
+  exportExcel?: boolean
+  importExcel?: boolean
+
   disableAdd?: boolean
   onClickAdd?: () => void
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -19,15 +23,17 @@ export interface StdCurdProps {
 }
 
 const props = defineProps<StdTableProps & StdCurdProps>()
-
-const { $gettext } = gettext
-
+const route = useRoute()
+const router = useRouter()
 const visible = ref(false)
 const update = ref(0)
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const data: any = reactive({ id: null })
+const modifyMode = ref(true)
+const editMode = ref<string>()
 
 provide('data', data)
+provide('editMode', editMode)
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const error: any = reactive({})
@@ -50,8 +56,22 @@ function add() {
 
   clear_error()
   visible.value = true
+  editMode.value = 'create'
+  modifyMode.value = true
 }
 const table = ref()
+
+const selectedRowKeys = ref([])
+
+const getParams = reactive({
+  trash: false,
+})
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const setParams = (k: string, v: any) => {
+  getParams[k] = v
+}
+
 function get_list() {
   table.value?.get_list()
 }
@@ -60,6 +80,8 @@ defineExpose({
   add,
   get_list,
   data,
+  getParams,
+  setParams,
 })
 
 function clear_error() {
@@ -68,18 +90,28 @@ function clear_error() {
   })
 }
 
-const ok = async () => {
+const stdEntryRef = ref()
+async function ok() {
+  const { formRef } = stdEntryRef.value
+
   clear_error()
-  await props?.beforeSave?.(data)
-  props.api!.save(data.id, data).then(r => {
-    message.success($gettext('Save Successfully'))
-    Object.assign(data, r)
-    get_list()
-    visible.value = false
-  }).catch(e => {
-    message.error($gettext(e?.message ?? 'Server error'), 5)
-    Object.assign(error, e.errors)
-  })
+  try {
+    await formRef.validateFields()
+    await props?.beforeSave?.(data)
+    props
+      .api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } })
+      .then(r => {
+        message.success($gettext('Save successfully'))
+        Object.assign(data, r)
+        get_list()
+        visible.value = false
+      })
+      .catch(e => {
+        message.error($gettext(e?.message ?? 'Server error'), 5)
+        Object.assign(error, e.errors)
+      })
+  }
+  catch { }
 }
 
 function cancel() {
@@ -88,48 +120,136 @@ function cancel() {
   clear_error()
 }
 
-function edit(id: number | string) {
-  props.api!.get(id).then(async r => {
-    Object.keys(data).forEach(k => {
-      delete data[k]
+watch(visible, v => {
+  if (!v) {
+    router.push({
+      query: {
+        ...route.query,
+        [`open.${props.rowKey || 'id'}`]: undefined,
+      },
     })
-    data.id = null
-    Object.assign(data, r)
+  }
+})
+
+watch(modifyMode, v => {
+  router.push({
+    query: {
+      ...route.query,
+      modify_mode: v.toString(),
+    },
+  })
+})
+
+function edit(id: number | string) {
+  get(id, true).then(() => {
     visible.value = true
+    modifyMode.value = true
+    editMode.value = 'modify'
   }).catch(e => {
     message.error($gettext(e?.message ?? 'Server error'), 5)
   })
 }
 
-const selectedRowKeys = ref([])
+function view(id: number | string) {
+  get(id, false).then(() => {
+    visible.value = true
+    modifyMode.value = false
+  }).catch(e => {
+    message.error($gettext(e?.message ?? 'Server error'), 5)
+  })
+}
+
+async function get(id: number | string, _modifyMode: boolean) {
+  return props
+    .api!.get(id, { ...props.overwriteParams })
+    .then(async r => {
+      Object.keys(data).forEach(k => {
+        delete data[k]
+      })
+      data.id = null
+      Object.assign(data, r)
+      if (!props.disabledModify) {
+        await router.push({
+          query: {
+            ...route.query,
+            [`open.${props.rowKey || 'id'}`]: id,
+            modify_mode: _modifyMode.toString(),
+          },
+        })
+      }
+    })
+}
+
+onMounted(async () => {
+  const id = route.query[`open.${props.rowKey || 'id'}`] as string
+  const _modifyMode = (route.query.modify_mode as string) === 'true'
+  if (id && !props.disabledModify && _modifyMode)
+    edit(id)
+
+  if (id && !props.disabledView && !_modifyMode)
+    view(id)
+})
+
+const modalTitle = computed(() => {
+  return data.id ? modifyMode.value ? $gettext('Modify') : $gettext('View Details') : $gettext('Add')
+})
+
 </script>
 
 <template>
   <div class="std-curd">
-    <ACard :title="title || $gettext('Table')">
+    <ACard>
+      <template #title>
+        <div class="flex items-center">
+          {{ title || $gettext('List') }}
+          <slot name="title-slot" />
+        </div>
+      </template>
       <template #extra>
         <ASpace>
+          <slot name="beforeAdd" />
           <a
             v-if="!disableAdd"
             @click="add"
           >{{ $gettext('Add') }}</a>
-
           <slot name="extra" />
+          <template v-if="!disableDelete">
+            <a
+              v-if="!getParams.trash"
+              @click="getParams.trash = true"
+            >
+              {{ $gettext('Trash') }}
+            </a>
+            <a
+              v-else
+              @click="getParams.trash = false"
+            >
+              {{ $gettext('List') }}
+            </a>
+          </template>
         </ASpace>
       </template>
 
       <StdTable
         ref="table"
-        v-bind="props"
         :key="update"
         v-model:selected-row-keys="selectedRowKeys"
+        v-bind="{
+          ...props,
+          getParams,
+        }"
         @click-edit="edit"
+        @click-view="view"
         @selected="onSelect"
       >
-        <template #actions="slotProps">
+        <template
+          v-for="(_, key) in $slots"
+          :key="key"
+          #[key]="slotProps"
+        >
           <slot
-            name="actions"
-            :actions="slotProps.record"
+            :name="key"
+            v-bind="slotProps"
           />
         </template>
       </StdTable>
@@ -137,34 +257,55 @@ const selectedRowKeys = ref([])
 
     <AModal
       class="std-curd-edit-modal"
-      :mask="false"
-      :title="data.id ? $gettext('Modify') : $gettext('Add')"
+      :mask="modalMask"
+      :title="modalTitle"
       :open="visible"
       :cancel-text="$gettext('Cancel')"
-      :ok-text="$gettext('OK')"
+      :ok-text="$gettext('Ok')"
       :width="modalMaxWidth"
+      :footer="modifyMode ? undefined : null"
       destroy-on-close
       @cancel="cancel"
       @ok="ok"
     >
       <div
-        v-if="$slots.beforeEdit"
-        class="before-edit"
+        v-if="!disabledModify && !disabledView"
+        class="m-2 flex justify-end"
       >
-        <slot
-          name="beforeEdit"
-          :data="data"
+        <ASwitch
+          v-model:checked="modifyMode"
+          class="mr-2"
         />
+        {{ modifyMode ? $gettext('Modify Mode') : $gettext('View Mode') }}
       </div>
 
-      <StdDataEntry
-        :data-list="editableColumns"
-        :data-source="data"
-        :error="error"
-      />
+      <template v-if="modifyMode">
+        <div
+          v-if="$slots.beforeEdit"
+          class="before-edit"
+        >
+          <slot
+            name="beforeEdit"
+            :data="data"
+          />
+        </div>
+
+        <StdDataEntry
+          ref="stdEntryRef"
+          :data-list="editableColumns"
+          :data-source="data"
+          :error="error"
+        />
+
+        <slot
+          name="edit"
+          :data="data"
+        />
+      </template>
 
-      <slot
-        name="edit"
+      <StdCurdDetail
+        v-else
+        :columns="columns"
         :data="data"
       />
     </AModal>

+ 36 - 0
app/src/components/StdDesign/StdDataDisplay/StdCurdDetail.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import type { ComputedRef } from 'vue'
+import _ from 'lodash'
+import type { Column } from '@/components/StdDesign/types'
+import { labelRender } from '@/components/StdDesign/StdDataEntry'
+import { CustomRender } from '@/components/StdDesign/StdDataDisplay/components/CustomRender'
+
+const props = defineProps<{
+  columns: Column[]
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  data: any
+}>()
+
+const displayColumns: ComputedRef<Column[]> = computed(() => {
+  return props.columns.filter(c => !c.hiddenInDetail && c.dataIndex !== 'action')
+})
+</script>
+
+<template>
+  <ADescriptions
+    :column="1"
+    bordered
+  >
+    <ADescriptionsItem
+      v-for="(c, index) in displayColumns"
+      :key="index"
+      :label="labelRender(c.title)"
+    >
+      <CustomRender v-bind="{ column: c, record: data, index, text: _.get(data, c.dataIndex!), isDetail: true }" />
+    </ADescriptionsItem>
+  </ADescriptions>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 3 - 2
app/src/components/StdDesign/StdDataDisplay/StdPagination.vue

@@ -1,10 +1,10 @@
 <script setup lang="ts">
-
 import type { Pagination } from '@/api/curd'
 
 const props = defineProps<{
   pagination: Pagination
-  size?: string
+  size?: 'default' | 'small'
+  loading: boolean
 }>()
 
 const emit = defineEmits(['change', 'changePageSize', 'update:pagination'])
@@ -31,6 +31,7 @@ const pageSize = computed({
   >
     <APagination
       v-model:pageSize="pageSize"
+      :disabled="loading"
       :current="pagination.current_page"
       :size="size"
       :total="pagination.total"

+ 270 - 105
app/src/components/StdDesign/StdDataDisplay/StdTable.vue

@@ -1,14 +1,16 @@
 <script setup lang="ts">
 import { message } from 'ant-design-vue'
 import { HolderOutlined } from '@ant-design/icons-vue'
-import { useGettext } from 'vue3-gettext'
 import type { ComputedRef, Ref } from 'vue'
-import type { SorterResult } from 'ant-design-vue/lib/table/interface'
+import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface'
+import type { FilterValue } from 'ant-design-vue/es/table/interface'
+import type { Key } from 'ant-design-vue/es/_util/type'
+import type { RouteParams } from 'vue-router'
+import _ from 'lodash'
 import StdPagination from './StdPagination.vue'
 import StdDataEntry from '@/components/StdDesign/StdDataEntry'
 import type { Pagination } from '@/api/curd'
 import type { Column } from '@/components/StdDesign/types'
-import exportCsvHandler from '@/components/StdDesign/StdDataDisplay/methods/exportCsv'
 import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
 import type Curd from '@/api/curd'
 
@@ -25,16 +27,16 @@ export interface StdTableProps {
   disableQueryParams?: boolean
   disableSearch?: boolean
   pithy?: boolean
-  exportCsv?: boolean
+  exportExcel?: boolean
+  exportMaterial?: boolean
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   overwriteParams?: Record<string, any>
+  disabledView?: boolean
   disabledModify?: boolean
   selectionType?: string
   sortable?: boolean
   disableDelete?: boolean
   disablePagination?: boolean
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  selectedRowKeys?: any | any[]
   sortableMoveHook?: (oldRow: number[], newRow: number[]) => boolean
   scrollX?: string | number
 }
@@ -43,18 +45,21 @@ const props = withDefaults(defineProps<StdTableProps>(), {
   rowKey: 'id',
 })
 
-const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys', 'clickBatchModify'])
-const { $gettext } = useGettext()
+const emit = defineEmits(['clickEdit', 'clickView', 'clickBatchModify', 'update:selectedRowKeys'])
 const route = useRoute()
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const dataSource: Ref<any[]> = ref([])
-const expandKeysList: Ref<number[]> = ref([])
+const expandKeysList: Ref<Key[]> = ref([])
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const rowsKeyIndexMap: Ref<Record<number, any>> = ref({})
 const loading = ref(true)
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const selectedRecords: Ref<Record<any, any>> = ref({})
 
 // This can be useful if there are more than one StdTable in the same page.
 const randomId = ref(Math.random().toString(36).substring(2, 8))
+const updateFilter = ref(0)
+const init = ref(false)
 
 const pagination: Pagination = reactive({
   total: 1,
@@ -67,40 +72,59 @@ const params = reactive({
   ...props.getParams,
 })
 
-const selectedKeysLocalBuffer = ref([])
+const get_list = _.debounce(_get_list, 200, {
+  leading: true,
+  trailing: false,
+})
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const selectedRowKeys = defineModel<any[]>('selectedRowKeys', {
+  default: () => [],
+})
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const selectedRows = defineModel<any[]>('selectedRows', {
+  type: Array,
+  default: () => [],
+})
 
-const selectedRowKeysBuffer = computed({
-  get() {
-    return props.selectedRowKeys || selectedKeysLocalBuffer.value
-  },
-  set(v) {
-    selectedKeysLocalBuffer.value = v
-    emit('update:selectedRowKeys', v)
-  },
+onMounted(() => {
+  selectedRows.value.forEach(v => {
+    selectedRecords.value[v[props.rowKey]] = v
+  })
 })
 
 const searchColumns = computed(() => {
   const _searchColumns: Column[] = []
 
-  props.columns?.forEach(column => {
-    if (column.search)
-      _searchColumns.push(column)
+  props.columns.forEach((column: Column) => {
+    if (column.search) {
+      if (typeof column.search === 'object') {
+        _searchColumns.push({
+          ...column,
+          edit: column.search,
+        })
+      }
+      else {
+        _searchColumns.push({ ...column })
+      }
+    }
   })
 
   return _searchColumns
 })
 
-const pithyColumns = computed(() => {
+const pithyColumns = computed<Column[]>(() => {
   if (props.pithy) {
     return props.columns?.filter(c => {
-      return c.pithy === true && !c.hidden
+      return c.pithy === true && !c.hiddenInTable
     })
   }
 
   return props.columns?.filter(c => {
-    return !c.hidden
+    return !c.hiddenInTable
   })
-}) as ComputedRef<Column[]>
+})
 
 const batchColumns = computed(() => {
   const batch: Column[] = []
@@ -113,18 +137,32 @@ const batchColumns = computed(() => {
   return batch
 })
 
+watch(route, () => {
+  params.trash = route.query.trash === 'true'
+})
+
 onMounted(() => {
-  if (!props.disableQueryParams)
-    Object.assign(params, route.query)
+  if (!props.disableQueryParams) {
+    Object.assign(params, {
+      ...route.query,
+      trash: route.query.trash === 'true',
+    })
+  }
 
   get_list()
 
   if (props.sortable)
     initSortable()
+
+  if (!selectedRowKeys.value?.length)
+    selectedRowKeys.value = []
+
+  init.value = true
 })
 
 defineExpose({
   get_list,
+  pagination,
 })
 
 function destroy(id: number | string) {
@@ -136,27 +174,15 @@ function destroy(id: number | string) {
   })
 }
 
-function get_list(page_num = null, page_size = 20) {
-  loading.value = true
-  if (page_num) {
-    params.page = page_num
-    params.page_size = page_size
-  }
-  props.api?.get_list(params).then(async r => {
-    dataSource.value = r.data
-    rowsKeyIndexMap.value = {}
-    if (props.sortable)
-
-      buildIndexMap(r.data)
-
-    if (r.pagination)
-      Object.assign(pagination, r.pagination)
-
-    loading.value = false
+function recover(id: number | string) {
+  props.api.recover(id).then(() => {
+    message.success($gettext('Recovered Successfully'))
+    get_list()
   }).catch(e => {
     message.error(e?.message ?? $gettext('Server error'))
   })
 }
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
   if (data && data.length > 0) {
@@ -172,11 +198,37 @@ function buildIndexMap(data: any, level: number = 0, index: number = 0, total: n
     })
   }
 }
-function orderPaginationChange(_pagination: Pagination, _: never, sorter: SorterResult) {
+
+async function _get_list(page_num = null, page_size = 20) {
+  loading.value = true
+  if (page_num) {
+    params.page = page_num
+    params.page_size = page_size
+  }
+  props.api?.get_list({ ...route.query, ...params, ...props.overwriteParams }).then(async r => {
+    dataSource.value = r.data
+    rowsKeyIndexMap.value = {}
+    if (props.sortable)
+      buildIndexMap(r.data)
+
+    if (r.pagination)
+      Object.assign(pagination, r.pagination)
+
+    setTimeout(() => {
+      loading.value = false
+    }, 200)
+  }).catch(e => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function onTableChange(_pagination: TablePaginationConfig, filters: Record<string, FilterValue>, sorter: SorterResult | SorterResult<any>[]) {
   if (sorter) {
-    selectedRowKeysBuffer.value = []
-    params.order_by = sorter.field
-    params.sort = sorter.order === 'ascend' ? 'asc' : 'desc'
+    sorter = sorter as SorterResult
+    selectedRowKeys.value = []
+    params.sort_by = sorter.field
+    params.order = sorter.order === 'ascend' ? 'asc' : 'desc'
     switch (sorter.order) {
       case 'ascend':
         params.sort = 'asc'
@@ -189,44 +241,98 @@ function orderPaginationChange(_pagination: Pagination, _: never, sorter: Sorter
         break
     }
   }
+  if (filters) {
+    Object.keys(filters).forEach((v: string) => {
+      params[v] = filters[v]
+    })
+  }
   if (_pagination)
-    selectedRowKeysBuffer.value = []
+    selectedRowKeys.value = []
 }
 
-function expandedTable(keys: number[]) {
+function expandedTable(keys: Key[]) {
   expandKeysList.value = keys
 }
 
-const crossPageSelect: Record<string, number[]> = {}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any,sonarjs/cognitive-complexity
+async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
+  if (props.selectionType === 'checkbox' || props.exportExcel) {
+    if (selected) {
+      _selectedRows.forEach(v => {
+        if (v) {
+          if (selectedRecords.value[v[props.rowKey]] === undefined)
+            selectedRowKeys.value.push(v[props.rowKey])
+
+          selectedRecords.value[v[props.rowKey]] = v
+        }
+      })
+    }
+    else {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      selectedRowKeys.value = selectedRowKeys.value.filter((v: any) => v !== record[props.rowKey])
+      delete selectedRecords.value[record[props.rowKey]]
+    }
 
-async function onSelectChange(_selectedRowKeys: number[]) {
-  const page = params.page || 1
+    await nextTick(async () => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const filteredRows: any[] = []
 
-  crossPageSelect[page] = _selectedRowKeys
+      selectedRowKeys.value.forEach(v => {
+        filteredRows.push(selectedRecords.value[v])
+      })
+      selectedRows.value = filteredRows
+    })
+  }
+  else {
+    if (selected) {
+      selectedRowKeys.value = record[props.rowKey]
+      selectedRows.value = [record]
+    }
+    else {
+      selectedRowKeys.value = []
+      selectedRows.value = []
+    }
+  }
+}
 
-  let t: number[] = []
-  Object.keys(crossPageSelect).forEach((v: string) => {
-    t.push(...crossPageSelect[v])
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) {
+  // console.log(selected, selectedRows, changeRows)
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  changeRows.forEach((v: any) => {
+    if (v) {
+      if (selected) {
+        selectedRowKeys.value.push(v[props.rowKey])
+        selectedRecords.value[v[props.rowKey]] = v
+      }
+      else {
+        delete selectedRecords.value[v[props.rowKey]]
+      }
+    }
   })
 
-  const n = [..._selectedRowKeys]
+  if (!selected) {
+    selectedRowKeys.value = selectedRowKeys.value.filter(v => {
+      return selectedRecords.value[v]
+    })
+  }
 
-  t = t.concat(n)
+  // console.log(selectedRowKeysBuffer.value, selectedRecords.value)
 
-  // console.log(crossPageSelect)
-  const set = new Set(t)
+  await nextTick(async () => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const filteredRows: any[] = []
 
-  selectedRowKeysBuffer.value = Array.from(set)
-  emit('onSelected', selectedRowKeysBuffer.value)
-}
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-function onSelect(record: any) {
-  emit('onSelectedRecord', record)
+    selectedRowKeys.value.forEach(v => {
+      filteredRows.push(selectedRecords.value[v])
+    })
+    selectedRows.value = filteredRows
+  })
 }
 
 const router = useRouter()
 
-const reset_search = async () => {
+const resetSearch = async () => {
   Object.keys(params).forEach(v => {
     delete params[v]
   })
@@ -237,65 +343,99 @@ const reset_search = async () => {
 
   router.push({ query: {} }).catch(() => {
   })
+
+  updateFilter.value++
 }
 
-watch(params, () => {
-  if (!props.disableQueryParams)
-    router.push({ query: params })
+watch(params, async v => {
+  if (init.value) {
+    await nextTick(() => {
+      get_list()
+    })
 
-  get_list()
+    if (!props.disableQueryParams)
+      await router.push({ query: v as RouteParams })
+  }
 })
 
+if (props.getParams) {
+  const getParams = computed(() => props.getParams)
+
+  watch(getParams, () => {
+    Object.assign(params, {
+      ...props.getParams,
+      page: 1,
+    })
+  }, { deep: true })
+}
+
+if (props.overwriteParams) {
+  const overwriteParams = computed(() => props.overwriteParams)
+
+  watch(overwriteParams, () => {
+    Object.assign(params, {
+      page: 1,
+    })
+    if (params.page === 1)
+      get_list()
+  }, { deep: true })
+}
+
 const rowSelection = computed(() => {
-  if (batchColumns.value.length > 0 || props.selectionType) {
+  if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) {
     return {
-      selectedRowKeys: selectedRowKeysBuffer.value,
-      onChange: onSelectChange,
+      selectedRowKeys: selectedRowKeys.value,
       onSelect,
-      type: batchColumns.value.length > 0 ? 'checkbox' : props.selectionType,
+      onSelectAll,
+      type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType,
     }
   }
   else {
     return null
   }
-})
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+}) as ComputedRef<any>
 
 const hasSelectedRow = computed(() => {
-  return batchColumns.value.length > 0 && selectedRowKeysBuffer.value.length > 0
+  return batchColumns.value.length > 0 && selectedRowKeys.value.length > 0
 })
 
 function clickBatchEdit() {
-  emit('clickBatchModify', batchColumns.value, selectedRowKeysBuffer.value)
+  emit('clickBatchModify', batchColumns.value, selectedRowKeys.value)
 }
 
 function initSortable() {
   useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
 }
 
-function export_csv() {
-  exportCsvHandler(props, pithyColumns)
+function changePage(page: number, page_size: number) {
+  Object.assign(params, {
+    page,
+    page_size,
+  })
 }
+
+const paginationSize = computed(() => {
+  if (props.size === 'small')
+    return 'small'
+  else
+    return 'default'
+})
 </script>
 
 <template>
   <div class="std-table">
     <StdDataEntry
       v-if="!disableSearch && searchColumns.length"
+      :key="updateFilter"
       :data-list="searchColumns"
       :data-source="params"
+      type="search"
       layout="inline"
     >
       <template #action>
         <ASpace class="action-btn">
-          <AButton
-            v-if="props.exportCsv"
-            type="primary"
-            ghost
-            @click="export_csv"
-          >
-            {{ $gettext('Export') }}
-          </AButton>
-          <AButton @click="reset_search">
+          <AButton @click="resetSearch">
             {{ $gettext('Reset') }}
           </AButton>
           <AButton
@@ -304,11 +444,12 @@ function export_csv() {
           >
             {{ $gettext('Batch Modify') }}
           </AButton>
+          <slot name="append-search" />
         </ASpace>
       </template>
     </StdDataEntry>
     <ATable
-      id="std-table"
+      :id="`std-table-${randomId}`"
       :columns="pithyColumns"
       :data-source="dataSource"
       :loading="loading"
@@ -316,18 +457,32 @@ function export_csv() {
       :row-key="rowKey"
       :row-selection="rowSelection"
       :scroll="{ x: scrollX }"
-      :size="size"
+      :size="size as any"
       :expanded-row-keys="expandKeysList"
-      @change="orderPaginationChange"
+      @change="onTableChange"
       @expanded-rows-change="expandedTable"
     >
-      <template #bodyCell="{ text, record, column }">
+      <template #bodyCell="{ text, record, column }: {text: any, record: Record<string, any>, column: any}">
         <template v-if="column.handle === true">
           <span class="ant-table-drag-icon"><HolderOutlined /></span>
           {{ text }}
         </template>
         <template v-if="column.dataIndex === 'action'">
-          <template v-if="!props.disabledModify">
+          <template v-if="!props.disabledView && !params.trash">
+            <AButton
+              type="link"
+              size="small"
+              @click="$emit('clickView', record[props.rowKey], record)"
+            >
+              {{ $gettext('View') }}
+            </AButton>
+            <ADivider
+              v-if="!props.disabledModify"
+              type="vertical"
+            />
+          </template>
+
+          <template v-if="!props.disabledModify && !params.trash">
             <AButton
               type="link"
               size="small"
@@ -348,9 +503,10 @@ function export_csv() {
 
           <template v-if="!props.disableDelete">
             <APopconfirm
+              v-if="!params.trash"
               :cancel-text="$gettext('No')"
               :ok-text="$gettext('OK')"
-              :title="$gettext('Are you sure you want to delete?')"
+              :title="$gettext('Are you sure you want to delete this item?')"
               @confirm="destroy(record[rowKey])"
             >
               <AButton
@@ -360,15 +516,30 @@ function export_csv() {
                 {{ $gettext('Delete') }}
               </AButton>
             </APopconfirm>
+            <APopconfirm
+              v-else
+              :cancel-text="$gettext('No')"
+              :ok-text="$gettext('OK')"
+              :title="$gettext('Are you sure you want to recover this item?')"
+              @confirm="recover(record[rowKey])"
+            >
+              <AButton
+                type="link"
+                size="small"
+              >
+                {{ $gettext('Recover') }}
+              </AButton>
+            </APopconfirm>
           </template>
         </template>
       </template>
     </ATable>
     <StdPagination
-      :size="size"
+      :size="paginationSize"
+      :loading="loading"
       :pagination="pagination"
-      @change="get_list"
-      @change-page-size="orderPaginationChange"
+      @change="changePage"
+      @change-page-size="onTableChange"
     />
   </div>
 </template>
@@ -390,12 +561,6 @@ function export_csv() {
   min-width: 90px;
 }
 
-.std-table {
-  .ant-table-wrapper {
-    // overflow-x: scroll;
-  }
-}
-
 .action-btn {
   // min-height: 50px;
   height: 100%;

+ 64 - 10
app/src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx

@@ -1,6 +1,8 @@
 // text, record, index, column
 import dayjs from 'dayjs'
 import type { JSX } from 'vue/jsx-runtime'
+import { Tag } from 'ant-design-vue'
+import { get } from 'lodash'
 
 export interface customRender {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -11,6 +13,8 @@ export interface customRender {
   index: any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   column: any
+  isExport?: boolean
+  isDetail?: boolean
 }
 
 export const datetime = (args: customRender) => {
@@ -18,18 +22,68 @@ export const datetime = (args: customRender) => {
 }
 
 export const date = (args: customRender) => {
-  return dayjs(args.text).format('YYYY-MM-DD')
+  return args.text ? dayjs(args.text).format('YYYY-MM-DD') : '-'
 }
+
+// Used in Export
+date.isDate = true
+datetime.isDatetime = true
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const mask = (maskObj: any): (args: customRender) => JSX.Element => {
+  return (args: customRender) => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    let v: any
+
+    if (typeof maskObj?.[args.text] === 'function')
+      v = maskObj[args.text]()
+    else if (typeof maskObj?.[args.text] === 'string')
+      v = maskObj[args.text]
+    else v = args.text
+
+    return v ?? '-'
+  }
+}
+
+export const arrayToTextRender = (args: customRender) => {
+  return args.text?.join(', ')
+}
+export const actualValueRender = (args: customRender, actualDataIndex: string | string[]) => {
+  return get(args.record, actualDataIndex)
+}
+
+export const longTextWithEllipsis = (len: number): (args: customRender) => JSX.Element => {
+  return (args: customRender) => {
+    if (args.isExport || args.isDetail)
+      return args.text
+
+    return args.text.length > len ? `${args.text.substring(0, len)}...` : args.text
+  }
+}
+
+export const year = (args: customRender) => {
+  return dayjs(args.text).format('YYYY')
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const mask = (args: customRender, maskObj: any): JSX.Element => {
-  let v
+export const maskRenderWithColor = (maskObj: any) => (args: customRender) => {
+  let label: string
+  if (typeof maskObj[args.text] === 'function')
+    label = maskObj[args.text]()
+  else if (typeof maskObj[args.text] === 'string')
+    label = maskObj[args.text]
+  else label = args.text
+
+  if (args.isExport)
+    return label
 
-  if (typeof maskObj?.[args.text] === 'function')
-    v = maskObj[args.text]()
-  else if (typeof maskObj?.[args.text] === 'string')
-    v = maskObj[args.text]
-  else
-    v = args.text
+  const colorMap = {
+    0: '',
+    1: 'blue',
+    2: 'green',
+    3: 'red',
+    4: 'cyan',
+  }
 
-  return <div>{v}</div>
+  return args.text ? h(Tag, { color: colorMap[args.text] }, maskObj[args.text]) : '-'
 }

+ 8 - 0
app/src/components/StdDesign/StdDataDisplay/components/CustomRender.tsx

@@ -0,0 +1,8 @@
+import _ from 'lodash'
+import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+
+export function CustomRender(props: customRender) {
+  return props.column.customRender
+    ? props.column.customRender(props)
+    : _.get(props.record, props.column.dataIndex!)
+}

+ 18 - 18
app/src/components/StdDesign/StdDataDisplay/methods/exportCsv.ts

@@ -4,12 +4,10 @@ import type { ComputedRef } from 'vue'
 import _ from 'lodash'
 import { downloadCsv } from '@/lib/helper'
 import type { Column, StdTableResponse } from '@/components/StdDesign/types'
-import gettext from '@/gettext'
 import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 
-const { $gettext } = gettext
 async function exportCsv(props: StdTableProps, pithyColumns: ComputedRef<Column[]>) {
-  const header: { title?: string; key: string | string[] }[] = []
+  const header: { title?: string; key: Column['dataIndex'] }[] = []
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   const headerKeys: any[] = []
   const showColumnsMap: Record<string, Column> = {}
@@ -24,8 +22,8 @@ async function exportCsv(props: StdTableProps, pithyColumns: ComputedRef<Column[
       title: t,
       key: column.dataIndex,
     })
-    headerKeys.push(column.dataIndex.toString())
-    showColumnsMap[column.dataIndex.toString()] = column
+    headerKeys.push(column?.dataIndex?.toString())
+    showColumnsMap[column?.dataIndex?.toString() as string] = column
   })
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -34,17 +32,20 @@ async function exportCsv(props: StdTableProps, pithyColumns: ComputedRef<Column[
   let page = 1
   while (hasMore) {
     // 准备 DataSource
-    await props.api!.get_list({ page }).then((r: StdTableResponse) => {
-      if (r.data.length === 0) {
-        hasMore = false
+    await props
+      .api!.get_list({ page })
+      .then((r: StdTableResponse) => {
+        if (r.data.length === 0) {
+          hasMore = false
 
-        return
-      }
-      dataSource.push(...r.data)
-    }).catch((e: { message?: string }) => {
-      message.error(e.message ?? $gettext('Server error'))
-      hasMore = false
-    })
+          return
+        }
+        dataSource.push(...r.data)
+      })
+      .catch((e: { message?: string }) => {
+        message.error(e.message ?? $gettext('Server error'))
+        hasMore = false
+      })
     page += 1
   }
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -59,13 +60,12 @@ async function exportCsv(props: StdTableProps, pithyColumns: ComputedRef<Column[
       const c = showColumnsMap[key]
 
       _data = c?.customRender?.({ text: _data }) ?? _data
-      _.set(obj, c.dataIndex, _data)
+      _.set(obj, c.dataIndex as string, _data)
     })
     data.push(obj)
   })
 
-  downloadCsv(header, data,
-    `${$gettext('Export')}-${props.title}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
+  downloadCsv(header, data, `${$gettext('Export')}-${props.title}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
 }
 
 export default exportCsv

+ 12 - 12
app/src/components/StdDesign/StdDataDisplay/methods/sortable.ts

@@ -1,14 +1,12 @@
 import { message } from 'ant-design-vue'
-import SortableJs from 'sortablejs'
 import type { Ref } from 'vue'
-import gettext from '@/gettext'
+import sortable from 'sortablejs'
+import type { Key } from 'ant-design-vue/es/_util/type'
 import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 
-const { $gettext } = gettext
-
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 function getRowKey(item: any) {
-  return item.children[0].children[0].dataset.rowKey
+  return item.dataset.rowKey
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -22,15 +20,16 @@ function getTargetData(data: any, indexList: number[]): any {
 
   return target
 }
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 function useSortable(props: StdTableProps, randomId: Ref<string>, dataSource: Ref<any[]>,
-  rowsKeyIndexMap: Ref<Record<number, number[]>>, expandKeysList: Ref<number[]>) {
+  rowsKeyIndexMap: Ref<Record<number, number[]>>, expandKeysList: Ref<Key[]>) {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   const table: any = document.querySelector(`#std-table-${randomId.value} tbody`)
 
-  // eslint-disable-next-line no-new
-  new SortableJs(table, {
-    handle: '.table-drag-icon',
+  // eslint-disable-next-line no-new,new-cap
+  new sortable(table, {
+    handle: '.ant-table-drag-icon',
     animation: 150,
     sort: true,
     forceFallback: true,
@@ -40,7 +39,7 @@ function useSortable(props: StdTableProps, randomId: Ref<string>, dataSource: Re
     onStart({ item }) {
       const targetRowKey = Number(getRowKey(item))
       if (targetRowKey)
-        expandKeysList.value = expandKeysList.value.filter((_item: number) => _item !== targetRowKey)
+        expandKeysList.value = expandKeysList.value.filter((_item: Key) => _item !== targetRowKey)
     },
     onMove({
       dragged,
@@ -112,8 +111,9 @@ function useSortable(props: StdTableProps, randomId: Ref<string>, dataSource: Re
         _rowIndex[level] += direction
         processChanges(getTargetData(dataSource.value, _rowIndex))
       }
-      console.log('Change row id', newRow.id, 'order', newRow.id, '=>', currentRow.id, ', direction: ', direction,
-        ', changes IDs:', changeIds)
+
+      // console.log('Change row id', newRow.id, 'order', newRow.id, '=>', currentRow.id, ', direction: ', direction,
+      //   ', changes IDs:', changeIds
 
       props.api.update_order({
         target_id: newRow.id,

+ 48 - 15
app/src/components/StdDesign/StdDataEntry/StdDataEntry.vue

@@ -1,13 +1,16 @@
 <script setup lang="tsx">
 import { Form } from 'ant-design-vue'
-import type { Column, JSXElements } from '@/components/StdDesign/types'
+import type { Ref } from 'vue'
+import type { Column, JSXElements, StdDesignEdit } from '@/components/StdDesign/types'
 import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
+import { labelRender } from '@/components/StdDesign/StdDataEntry'
 
 const props = defineProps<{
   dataList: Column[]
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   dataSource: Record<string, any>
   errors?: Record<string, string>
+  type?: 'search' | 'edit'
   layout?: 'horizontal' | 'vertical' | 'inline'
 }>()
 
@@ -27,13 +30,6 @@ const dataSource = computed({
 
 const slots = useSlots()
 
-function labelRender(title?: string | (() => string)) {
-  if (typeof title === 'function')
-    return title()
-
-  return title
-}
-
 function extraRender(extra?: string | (() => string)) {
   if (typeof extra === 'function')
     return extra()
@@ -41,21 +37,58 @@ function extraRender(extra?: string | (() => string)) {
   return extra
 }
 
+const formRef = ref<InstanceType<typeof Form>>()
+
+defineExpose({
+  formRef,
+})
+
 function Render() {
   const template: JSXElements = []
+  const isCreate = inject<Ref<string>>('editMode', ref(''))?.value === 'create'
 
   props.dataList.forEach((v: Column) => {
+    const dataIndex = (v.edit?.actualDataIndex ?? v.dataIndex) as string
+
+    dataSource.value[dataIndex] = props.dataSource[dataIndex]
+    if (props.type === 'search') {
+      if (v.search) {
+        const type = (v.search as StdDesignEdit)?.type || v.edit?.type
+
+        template.push(
+          <StdFormItem
+            label={labelRender(v.title)}
+            extra={extraRender(v.extra)}
+            error={props.errors}
+          >
+            {type?.(v.edit!, dataSource.value, v.dataIndex)}
+          </StdFormItem>,
+        )
+      }
+
+      return
+    }
+
+    // console.log(isCreate && v.hiddenInCreate, !isCreate && v.hiddenInModify)
+    if ((isCreate && v.hiddenInCreate) || (!isCreate && v.hiddenInModify))
+      return
+
     let show = true
     if (v.edit?.show && typeof v.edit.show === 'function')
       show = v.edit.show(props.dataSource)
 
     if (v.edit?.type && show) {
-      template.push(<StdFormItem
-        dataIndex={v.dataIndex}
-      label={labelRender(v.title)}
-      extra={extraRender(v.extra)}
-      error={props.errors}>
-        {v.edit.type(v.edit, dataSource.value, v.dataIndex)}
+      template.push(
+        <StdFormItem
+          key={dataIndex}
+          dataIndex={dataIndex}
+          label={labelRender(v.title)}
+          extra={extraRender(v.extra)}
+          error={props.errors}
+          required={v.edit?.config?.required}
+          hint={v.edit?.hint}
+        >
+          {v.edit.type(v.edit, dataSource.value, dataIndex)}
         </StdFormItem>,
       )
     }
@@ -64,7 +97,7 @@ function Render() {
   if (slots.action)
     template.push(<div class={'std-data-entry-action'}>{slots.action()}</div>)
 
-  return <Form layout={props.layout || 'vertical'}>{template}</Form>
+  return <Form ref={formRef} model={dataSource.value} layout={props.layout || 'vertical'}>{template}</Form>
 }
 </script>
 

+ 9 - 18
app/src/components/StdDesign/StdDataEntry/StdFormItem.vue

@@ -1,51 +1,42 @@
 <script setup lang="ts">
 import { computed } from 'vue'
-import { useGettext } from 'vue3-gettext'
+import type { Column } from '@/components/StdDesign/types'
 
 const props = defineProps<Props>()
 
-const { $gettext } = useGettext()
-
 export interface Props {
-  dataIndex?: string | string[]
+  dataIndex?: Column['dataIndex']
   label?: string
   extra?: string
+  hint?: string | (() => string)
   error?: {
     [key: string]: string
   }
+  required?: boolean
 }
 
 const tag = computed(() => {
   return props.error?.[props.dataIndex!.toString()] ?? ''
 })
 
-const valid_status = computed(() => {
-  if (tag.value)
-    return 'error'
-  else
-    return 'success'
-})
-
 const help = computed(() => {
   if (tag.value.includes('required'))
-    return () => $gettext('This field should not be empty')
+    return $gettext('This field should not be empty')
 
-  return () => {
-  }
+  return props.hint
 })
 </script>
 
 <template>
   <AFormItem
+    :name="dataIndex as string"
     :label="label"
-    :extra="extra"
-    :validate-status="valid_status"
-    :help="help?.()"
+    :help="help"
+    :required="required"
   >
     <slot />
   </AFormItem>
 </template>
 
 <style scoped lang="less">
-
 </style>

+ 0 - 2
app/src/components/StdDesign/StdDataEntry/components/StdPassword.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue'
-import { useGettext } from 'vue3-gettext'
 
 const props = defineProps<{
   value: string
@@ -20,7 +19,6 @@ const M_value = computed({
 })
 
 const visibility = ref(false)
-const { $gettext } = useGettext()
 function handle_generate() {
   visibility.value = true
   M_value.value = 'xxxx'

+ 52 - 29
app/src/components/StdDesign/StdDataEntry/components/StdSelect.vue

@@ -1,50 +1,73 @@
 <script setup lang="ts">
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import type { SelectProps } from 'ant-design-vue'
 
 const props = defineProps<{
-  value: string
-  mask: Record<string, string | (() => string)>
+  mask?: Record<string | number, string | (() => string)> | (() => Promise<Record<string | number, string>>)
+  placeholder?: string
+  multiple?: boolean
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  defaultValue?: any
 }>()
 
-const emit = defineEmits(['update:value'])
+const selectedValue = defineModel<string | number | string[] | number[]>('value')
+const options = ref<SelectProps['options']>([])
 
-const options = computed(() => {
-  const _options = ref<SelectProps['options']>([])
+const loadOptions = async () => {
+  options.value = []
+  let actualValue: number | string
+  if (typeof props.mask === 'function') {
+    const getOptions = props.mask as (() => Promise<Record<string | number, string>>)
 
-  for (const [key, value] of Object.entries(props.mask)) {
-    const v = value as () => string
+    const r = await getOptions()
+    for (const [value, label] of Object.entries(r)) {
+      actualValue = value
+      if (typeof selectedValue.value === 'number')
+        actualValue = Number(value)
+      options.value?.push({ label, value: actualValue })
+    }
 
-    _options.value!.push({ label: v?.(), value: key })
+    return
   }
+  for (const [value, label] of Object.entries(props.mask as Record<string | number, string | (() => string)>)) {
+    let actualLabel = label
 
-  return _options
-})
+    if (typeof label === 'function')
+      actualLabel = label()
+
+    actualValue = value
+    if (typeof selectedValue.value === 'number')
+      actualValue = Number(value)
+
+    options.value?.push({ label: actualLabel, value: actualValue })
+    if (actualValue === selectedValue.value)
+      selectedValue.value = actualValue
+  }
+}
+
+const init = () => {
+  loadOptions()
+}
+
+watch(props, init)
+
+onMounted(() => {
+  if (!selectedValue.value && props.defaultValue)
+    selectedValue.value = props.defaultValue
 
-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)
-  },
+  init()
 })
 </script>
 
 <template>
   <ASelect
-    v-model:value="_value"
-    :options="options.value"
+    v-model:value="selectedValue"
+    :options="options"
+    :placeholder="props.placeholder"
+    :default-active-first-option="false"
+    :mode="props.multiple ? 'multiple' : undefined"
     style="min-width: 180px"
+    :get-popup-container="triggerNode => triggerNode.parentNode"
   />
 </template>
 

+ 1 - 2
app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
-import gettext from '@/gettext'
 import type Curd from '@/api/curd'
 import type { Column } from '@/components/StdDesign/types'
 
@@ -19,7 +18,7 @@ const props = defineProps<{
 }>()
 
 const emit = defineEmits(['update:selectedKey', 'changeSelect'])
-const { $gettext } = gettext
+
 const visible = ref(false)
 const M_value = ref('')
 

+ 109 - 51
app/src/components/StdDesign/StdDataEntry/index.tsx

@@ -1,88 +1,128 @@
 import { h } from 'vue'
-import { Input, InputNumber, Switch, Textarea } from 'ant-design-vue'
-import _ from 'lodash'
+import {
+  DatePicker,
+  Input,
+  InputNumber,
+  RangePicker,
+  Switch,
+  Textarea,
+} from 'ant-design-vue'
+import type { Dayjs } from 'dayjs'
+import dayjs from 'dayjs'
 import StdDataEntry from './StdDataEntry.vue'
 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'
+import { DATE_FORMAT } from '@/constants'
 
-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))
+export function readonly(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return h('p', dataSource?.[dataIndex] ?? edit?.config?.defaultValue)
 }
 
-function placeholder_helper(edit: StdDesignEdit) {
+export function labelRender(title?: string | (() => string)) {
+  if (typeof title === 'function')
+    return title()
+
+  return title
+}
+
+export function placeholderHelper(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) {
+export function input(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
   return h(Input, {
-    'placeholder': placeholder_helper(edit),
-    'value': dataSource?.[dataIndex],
+    'autocomplete': 'off',
+    'placeholder': placeholderHelper(edit),
+    'value': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
     'onUpdate:value': value => {
       dataSource[dataIndex] = value
     },
   })
 }
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-function inputNumber(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+export function inputNumber(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  if (edit.config?.defaultValue !== undefined)
+    dataSource[dataIndex] = edit.config.defaultValue
+
   return h(InputNumber, {
-    'placeholder': placeholder_helper(edit),
+    'placeholder': placeholderHelper(edit),
     'min': edit.config?.min,
     'max': edit.config?.max,
-    'value': dataSource?.[dataIndex],
+    'value': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
     'onUpdate:value': value => {
       dataSource[dataIndex] = value
     },
+    'addon-before': edit.config?.addonBefore,
+    'addon-after': edit.config?.addonAfter,
+    'prefix': edit.config?.prefix,
+    'suffix': edit.config?.suffix,
   })
 }
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-function textarea(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+export function textarea(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
   return h(Textarea, {
-    'placeholder': placeholder_helper(edit),
-    'value': dataSource?.[dataIndex],
+    'placeholder': placeholderHelper(edit),
+    'value': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
     '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]}
-    value={dataSource[dataIndex]}
-    generate={edit.config?.generate}
-    placeholder={placeholder_helper(edit)}
-  />
+export function password(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return (
+    <StdPassword
+      v-model:value={dataSource[dataIndex]}
+      value={dataSource[dataIndex] ?? edit?.config?.defaultValue}
+      generate={edit.config?.generate}
+      placeholder={placeholderHelper(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]}
-    value={dataSource[dataIndex]}
-    mask={edit.mask as Record<string, () => string>}
-  />
+export function select(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  const actualDataIndex = edit?.actualDataIndex ?? dataIndex
+
+  return (
+    <StdSelect
+      v-model:value={dataSource[actualDataIndex]}
+      mask={edit.mask}
+      placeholder={placeholderHelper(edit)}
+      multiple={edit.select?.multiple}
+      defaultValue={edit.config?.defaultValue}
+    />
+  )
 }
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-function selector(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  return <StdSelector
-    v-model:selectedKey={dataSource[dataIndex]}
-    selectedKey={dataSource[dataIndex]}
-    recordValueIndex={edit.selector?.recordValueIndex}
-    selectionType={edit.selector?.selectionType}
-    api={edit.selector?.api}
-    columns={edit.selector?.columns}
-    disableSearch={edit.selector?.disableSearch}
-    getParams={edit.selector?.getParams}
-    description={edit.selector?.description}
-  />
+export function selector(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  return (
+    <StdSelector
+      v-model:selectedKey={dataSource[dataIndex]}
+      selectedKey={dataSource[dataIndex] || edit?.config?.defaultValue}
+      recordValueIndex={edit.selector?.recordValueIndex}
+      selectionType={edit.selector?.selectionType}
+      api={edit.selector?.api}
+      columns={edit.selector?.columns}
+      disableSearch={edit.selector?.disableSearch}
+      getParams={edit.selector?.getParams}
+      description={edit.selector?.description}
+    />
+  )
 }
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-function switcher(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+export function switcher(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
   return h(Switch, {
-    'checked': dataSource?.[dataIndex],
+    'checked': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     'onUpdate:checked': (value: any) => {
       dataSource[dataIndex] = value
@@ -90,15 +130,33 @@ function switcher(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
   })
 }
 
-export {
-  readonly,
-  input,
-  textarea,
-  select,
-  selector,
-  password,
-  inputNumber,
-  switcher,
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function datePicker(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  const date: Dayjs | undefined = dataSource?.[dataIndex] ? dayjs(dataSource?.[dataIndex]) : undefined
+
+  return (
+    <DatePicker
+      format={DATE_FORMAT}
+      value={date}
+      onChange={(_, dataString) => dataSource[dataIndex] = dataString ?? undefined}
+    />
+  )
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function dateRangePicker(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
+  const dates: [Dayjs, Dayjs] = dataSource
+    ?.[dataIndex]
+    ?.filter((item: string) => !!item)
+    ?.map((item: string) => dayjs(item))
+
+  return (
+    <RangePicker
+      format={DATE_FORMAT}
+      value={dates}
+      onChange={(_, dateStrings: [string, string]) => dataSource[dataIndex] = dateStrings}
+    />
+  )
 }
 
 export default StdDataEntry

+ 58 - 40
app/src/components/StdDesign/types.d.ts

@@ -1,25 +1,35 @@
 import Curd, {Pagination} from '@/api/curd'
-import { Ref } from 'vue'
-import type { JSX } from 'vue/jsx'
+import {Ref} from 'vue'
+import type {JSX} from 'vue/jsx'
+import {TableColumnType} from "ant-design-vue"
+
 export type JSXElements = JSX.Element[]
 
 export interface StdDesignEdit {
-  type?: function // component type
+  type?: (edit: StdDesignEdit, dataSource: any, dataIndex: any) => JSX.Element // component type
 
   show?: (dataSource: any) => boolean // show component or not
 
   batch?: boolean // batch edit
 
-  mask?: Record<string, () => string> // use for select-option
+  mask?: Record<string | number, string | (() => string)> | (() => Promise<Record<string | number, string>>) // use for select-option
 
   rules?: [] // validator rules
 
+  hint?: string | (() => string) // hint form item
+
+  actualDataIndex?: string
+
+  select?: {
+    multiple?: boolean
+  }
+
   selector?: {
     getParams?: {}
     recordValueIndex: any // relative to api return
     selectionType: any
-    api: Curd,
-    valueApi?: Curd,
+    api: Curd
+    valueApi?: Curd
     columns: any
     disableSearch?: boolean
     description?: string
@@ -28,6 +38,11 @@ export interface StdDesignEdit {
     dataSourceValueIndex?: any // relative to dataSource
   } // StdSelector Config
 
+  upload?: {
+    limit?: number // upload file limitation
+    action: string // upload url
+  }
+
   config?: {
     label?: string | (() => string) // label for form item
     size?: string // class size of Std image upload
@@ -36,7 +51,13 @@ export interface StdDesignEdit {
     min?: number // min value for input number
     max?: number // max value for input number
     error_messages?: Ref
-    hint?: string | (() => string) // hint form item
+    required?: boolean
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    defaultValue?: any
+    addonBefore?: string // for inputNumber
+    addonAfter?: string // for inputNumber
+    prefix?: string // for inputNumber
+    suffix?: string // for inputNumber
   }
 
   flex?: Flex
@@ -50,39 +71,36 @@ export interface Flex {
   xxl?: string | number | boolean
 }
 
-export interface Column {
-  title?: string | (() => string);
-  dataIndex: string | string[];
-  edit?: StdDesignEdit;
-  customRender?: function;
-  extra?: string | (() => string);
-  pithy?: boolean;
-  search?: boolean | StdDesignEdit;
-  sortable?: boolean;
-  hidden?: boolean;
-  width?: string | number;
-  handle?: boolean;
-  hiddenInTrash?: boolean;
-  hiddenInCreate?: boolean;
-  hiddenInModify?: boolean;
-  batch?: boolean;
-}
-
-
-export interface StdTableProvideData {
-  displayColumns: Column[];
-  pithyColumns: Column[];
-  columnsMap: { [key: string]: Column };
-  displayKeys: string[];
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  editItem: (id: number, data: any, index: string | number) => void;
-  deleteItem: (id: number, index: string | number) => void;
-  recoverItem: (id: number) => {};
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  params: any;
-  dataSource: any;
-  get_list: () => void;
-  loading: Ref<boolean>;
+export interface Column extends TableColumnType {
+  title?: string | (() => string)
+  edit?: StdDesignEdit
+  extra?: string | (() => string)
+  pithy?: boolean
+  search?: boolean | StdDesignEdit
+  sortable?: boolean
+  handle?: boolean
+  hiddenInTable?: boolean
+  hiddenInTrash?: boolean
+  hiddenInCreate?: boolean
+  hiddenInModify?: boolean
+  hiddenInDetail?: boolean
+  hiddenInExport?: boolean
+  import?: boolean
+  batch?: boolean
+  customRender?: function
+  selector?: {
+    getParams?: {}
+    recordValueIndex: any // relative to api return
+    selectionType: any
+    api: Curd
+    valueApi?: Curd
+    columns: any
+    disableSearch?: boolean
+    description?: string
+    bind?: any
+    itemKey?: any // default is id
+    dataSourceValueIndex?: any // relative to dataSource
+  }
 }
 
 export interface StdTableResponse {

+ 0 - 3
app/src/components/SwitchAppearance/SwitchAppearance.vue

@@ -1,14 +1,11 @@
 <script lang="ts" setup>
 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 VPSwitch from '@/components/VPSwitch/VPSwitch.vue'
 import { useSettingsStore } from '@/pinia'
 
-const { $gettext } = useGettext()
-
 const settings = useSettingsStore()
 const devicePrefersTheme = inject('devicePrefersTheme') as Ref<string>
 const isDark = computed(() => settings.theme === 'dark')

+ 1 - 2
app/src/constants/index.ts

@@ -1,6 +1,5 @@
-import gettext from '@/gettext'
+export const DATE_FORMAT = 'YYYY-MM-DD'
 
-const { $gettext } = gettext
 export enum AutoCertState {
   Disable = 0,
   Enable = 1,

+ 4 - 2
app/src/gettext.ts

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

+ 0 - 4
app/src/language/constants.ts

@@ -1,7 +1,3 @@
-import gettext from '@/gettext'
-
-const { $gettext } = gettext
-
 export const msg = [
   $gettext('The username or password is incorrect'),
   $gettext('Prohibit changing root password in demo'),

+ 0 - 3
app/src/layouts/HeaderLayout.vue

@@ -3,7 +3,6 @@ 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 auth from '@/api/auth'
 import NginxControl from '@/components/NginxControl/NginxControl.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
@@ -13,8 +12,6 @@ const emit = defineEmits<{
   clickUnFold: [void]
 }>()
 
-const { $gettext } = gettext
-
 const router = useRouter()
 
 function logout() {

+ 9 - 7
app/src/layouts/SideBar.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
-import type { ComputedRef } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
 import type { AntdIconType } from '@ant-design/icons-vue/lib/components/AntdIcon'
 import type { IconComponentProps } from '@ant-design/icons-vue/es/components/Icon'
+import type { Key } from 'ant-design-vue/es/_util/type'
 import Logo from '@/components/Logo/Logo.vue'
 import { routes } from '@/routes'
 import EnvIndicator from '@/components/EnvIndicator/EnvIndicator.vue'
@@ -10,7 +11,7 @@ const route = useRoute()
 
 const openKeys = ref([openSub()])
 
-const selectedKey = ref([route.name])
+const selectedKey = ref([route.name]) as Ref<Key[]>
 
 function openSub() {
   const path = route.path
@@ -20,7 +21,7 @@ function openSub() {
 }
 
 watch(route, () => {
-  selectedKey.value = [route.name]
+  selectedKey.value = [route.name as Key]
 
   const sub = openSub()
   const p = openKeys.value.indexOf(sub)
@@ -36,6 +37,7 @@ interface meta {
   icon: AntdIconType
   hiddenInSidebar: boolean
   hideChildren: boolean
+  name: () => string
 }
 
 interface sidebar {
@@ -54,7 +56,7 @@ const visible: ComputedRef<sidebar[]> = computed(() => {
 
     const t: sidebar = {
       path: s.path,
-      name: s.name,
+      name: s?.meta?.name ?? (() => ''),
       meta: s.meta as unknown as meta,
       children: [],
     };
@@ -90,7 +92,7 @@ const visible: ComputedRef<sidebar[]> = computed(() => {
           @click="$router.push(`/${s.path}`).catch(() => {})"
         >
           <Component :is="s.meta.icon as IconComponentProps" />
-          <span>{{ s.name() }}</span>
+          <span>{{ s.meta?.name() }}</span>
         </AMenuItem>
 
         <ASubMenu
@@ -99,14 +101,14 @@ const visible: ComputedRef<sidebar[]> = computed(() => {
         >
           <template #title>
             <Component :is="s.meta.icon as IconComponentProps" />
-            <span>{{ s.name() }}</span>
+            <span>{{ s?.meta?.name() }}</span>
           </template>
           <AMenuItem
             v-for="child in s.children"
             :key="child.name"
           >
             <RouterLink :to="`/${s.path}/${child.path}`">
-              {{ child.name() }}
+              {{ child?.meta?.name() }}
             </RouterLink>
           </AMenuItem>
         </ASubMenu>

+ 4 - 0
app/src/lib/http/index.ts

@@ -84,6 +84,10 @@ const http = {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     return instance.delete<any, any>(url, config)
   },
+  patch(url: string, config: AxiosRequestConfig = {}) {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    return instance.patch<any, any>(url, config)
+  },
 }
 
 export default http

+ 89 - 58
app/src/routes/index.ts

@@ -1,5 +1,5 @@
+import type { RouteRecordRaw } from 'vue-router'
 import { createRouter, createWebHashHistory } from 'vue-router'
-import type { AntDesignOutlinedIconType } from '@ant-design/icons-vue/lib/icons/AntDesignOutlined'
 
 import {
   BellOutlined,
@@ -17,253 +17,284 @@ import {
 } from '@ant-design/icons-vue'
 import NProgress from 'nprogress'
 
-import gettext from '@/gettext'
 import { useUserStore } from '@/pinia'
 
 import 'nprogress/nprogress.css'
 
-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: Route[] = [
+export const routes: RouteRecordRaw[] = [
   {
     path: '/',
-    name: () => $gettext('Home'),
+    name: 'Home',
     component: () => import('@/layouts/BaseLayout.vue'),
     redirect: '/dashboard',
+    meta: {
+      name: () => $gettext('Home'),
+    },
     children: [
       {
         path: 'dashboard',
         component: () => import('@/views/dashboard/DashBoard.vue'),
-        name: () => $gettext('Dashboard'),
+        name: 'Dashboard',
         meta: {
+          name: () => $gettext('Dashboard'),
           icon: HomeOutlined,
         },
       },
       {
         path: 'domain',
-        name: () => $gettext('Manage Sites'),
+        name: 'Manage Sites',
         component: () => import('@/layouts/BaseRouterView.vue'),
         meta: {
+          name: () => $gettext('Manage Sites'),
           icon: CloudOutlined,
         },
         redirect: '/domain/list',
         children: [{
           path: 'list',
-          name: () => $gettext('Sites List'),
+          name: 'Sites List',
           component: () => import('@/views/domain/DomainList.vue'),
+          meta: {
+            name: () => $gettext('Sites List'),
+          },
         }, {
           path: 'add',
-          name: () => $gettext('Add Site'),
+          name: 'Add Site',
           component: () => import('@/views/domain/DomainAdd.vue'),
+          meta: {
+            name: () => $gettext('Add Site'),
+          },
         }, {
           path: ':name',
-          name: () => $gettext('Edit Site'),
+          name: 'Edit Site',
           component: () => import('@/views/domain/DomainEdit.vue'),
           meta: {
+            name: () => $gettext('Edit Site'),
             hiddenInSidebar: true,
           },
         }],
       },
       {
         path: 'streams',
-        name: () => $gettext('Manage Streams'),
+        name: 'Manage Streams',
         component: () => import('@/views/stream/StreamList.vue'),
         meta: {
+          name: () => $gettext('Manage Streams'),
           icon: ShareAltOutlined,
         },
       },
       {
         path: 'stream/:name',
-        name: () => $gettext('Edit Stream'),
+        name: 'Edit Stream',
         component: () => import('@/views/stream/StreamEdit.vue'),
         meta: {
+          name: () => $gettext('Edit Stream'),
           hiddenInSidebar: true,
         },
       },
       {
         path: 'config',
-        name: () => $gettext('Manage Configs'),
+        name: 'Manage Configs',
         component: () => import('@/views/config/Config.vue'),
         meta: {
+          name: () => $gettext('Manage Configs'),
           icon: FileOutlined,
           hideChildren: true,
         },
       },
       {
         path: 'config/:name+/edit',
-        name: () => $gettext('Edit Configuration'),
+        name: 'Edit Configuration',
         component: () => import('@/views/config/ConfigEdit.vue'),
         meta: {
+          name: () => $gettext('Edit Configuration'),
           hiddenInSidebar: true,
         },
       },
       {
         path: 'certificates',
-        name: () => $gettext('Certificates'),
+        name: 'Certificates',
         component: () => import('@/layouts/BaseRouterView.vue'),
         redirect: '/certificates/list',
         meta: {
+          name: () => $gettext('Certificates'),
           icon: SafetyCertificateOutlined,
         },
         children: [
+          {
+            path: 'acme_users',
+            name: 'ACME User',
+            component: () => import('@/views/certificate/ACMEUser.vue'),
+            meta: {
+              name: () => $gettext('ACME User'),
+            },
+          },
           {
             path: 'list',
-            name: () => $gettext('Certificates List'),
+            name: 'Certificates List',
             component: () => import('@/views/certificate/Certificate.vue'),
+            meta: {
+              name: () => $gettext('Certificates List'),
+            },
           },
           {
             path: ':id',
-            name: () => $gettext('Modify Certificate'),
+            name: 'Modify Certificate',
             component: () => import('@/views/certificate/CertificateEditor.vue'),
             meta: {
+              name: () => $gettext('Modify Certificate'),
               hiddenInSidebar: true,
             },
           },
           {
             path: 'import',
-            name: () => $gettext('Import Certificate'),
+            name: 'Import Certificate',
             component: () => import('@/views/certificate/CertificateEditor.vue'),
             meta: {
+              name: () => $gettext('Import Certificate'),
               hiddenInSidebar: true,
             },
           },
           {
             path: 'dns_credential',
-            name: () => $gettext('DNS Credentials'),
+            name: 'DNS Credentials',
             component: () => import('@/views/certificate/DNSCredential.vue'),
+            meta: {
+              name: () => $gettext('DNS Credentials'),
+            },
           },
         ],
       },
       {
         path: 'terminal',
-        name: () => $gettext('Terminal'),
+        name: 'Terminal',
         component: () => import('@/views/pty/Terminal.vue'),
         meta: {
+          name: () => $gettext('Terminal'),
           icon: CodeOutlined,
         },
       },
       {
         path: 'nginx_log',
-        name: () => $gettext('Nginx Log'),
+        name: 'Nginx Log',
         meta: {
+          name: () => $gettext('Nginx Log'),
           icon: FileTextOutlined,
         },
         children: [{
           path: 'access',
-          name: () => $gettext('Access Logs'),
+          name: 'Access Logs',
           component: () => import('@/views/nginx_log/NginxLog.vue'),
+          meta: {
+            name: () => $gettext('Access Logs'),
+          },
         }, {
           path: 'error',
-          name: () => $gettext('Error Logs'),
+          name: 'Error Logs',
           component: () => import('@/views/nginx_log/NginxLog.vue'),
+          meta: {
+            name: () => $gettext('Error Logs'),
+          },
         }, {
           path: 'site',
-          name: () => $gettext('Site Logs'),
+          name: 'Site Logs',
           component: () => import('@/views/nginx_log/NginxLog.vue'),
           meta: {
+            name: () => $gettext('Site Logs'),
             hiddenInSidebar: true,
           },
         }],
       },
       {
         path: 'environment',
-        name: () => $gettext('Environment'),
+        name: 'Environment',
         component: () => import('@/views/environment/Environment.vue'),
         meta: {
+          name: () => $gettext('Environment'),
           icon: DatabaseOutlined,
         },
       },
       {
         path: 'notifications',
-        name: () => $gettext('Notifications'),
+        name: 'Notifications',
         component: () => import('@/views/notification/Notification.vue'),
         meta: {
+          name: () => $gettext('Notifications'),
           icon: BellOutlined,
         },
       },
       {
         path: 'user',
-        name: () => $gettext('Manage Users'),
+        name: 'Manage Users',
         component: () => import('@/views/user/User.vue'),
         meta: {
+          name: () => $gettext('Manage Users'),
           icon: UserOutlined,
         },
       },
       {
         path: 'preference',
-        name: () => $gettext('Preference'),
+        name: 'Preference',
         component: () => import('@/views/preference/Preference.vue'),
         meta: {
+          name: () => $gettext('Preference'),
           icon: SettingOutlined,
         },
       },
       {
         path: 'system',
-        name: () => $gettext('System'),
+        name: 'System',
         redirect: 'system/about',
         meta: {
+          name: () => $gettext('System'),
           icon: InfoCircleOutlined,
         },
         children: [{
           path: 'about',
-          name: () => $gettext('About'),
+          name: 'About',
           component: () => import('@/views/system/About.vue'),
+          meta: {
+            name: () => $gettext('About'),
+          },
         }, {
           path: 'upgrade',
-          name: () => $gettext('Upgrade'),
+          name: 'Upgrade',
           component: () => import('@/views/system/Upgrade.vue'),
+          meta: {
+            name: () => $gettext('Upgrade'),
+          },
         }],
       },
     ],
   },
   {
     path: '/install',
-    name: () => $gettext('Install'),
+    name: 'Install',
     component: () => import('@/views/other/Install.vue'),
-    meta: { noAuth: true },
+    meta: { name: () => $gettext('Install'), noAuth: true },
   },
   {
     path: '/login',
-    name: () => $gettext('Login'),
+    name: 'Login',
     component: () => import('@/views/other/Login.vue'),
-    meta: { noAuth: true },
+    meta: { name: () => $gettext('Login'), noAuth: true },
   },
   {
     path: '/:pathMatch(.*)*',
-    name: () => $gettext('Not Found'),
+    name: 'Not Found',
     component: () => import('@/views/other/Error.vue'),
-    meta: { noAuth: true, status_code: 404, error: () => $gettext('Not Found') },
+    meta: { name: () => $gettext('Not Found'), noAuth: true, status_code: 404, error: () => $gettext('Not Found') },
   },
 ]
 
 const router = createRouter({
   history: createWebHashHistory(),
-
-  // @ts-expect-error routes type error
   routes,
 })
 
 NProgress.configure({ showSpinner: false })
 
 router.beforeEach((to, _, next) => {
-  // @ts-expect-error name type
-  document.title = `${to.name?.()} | Nginx UI`
+  document.title = `${to?.meta.name?.() ?? ''} | Nginx UI`
 
   NProgress.start()
 

+ 20 - 0
app/src/routes/type.d.ts

@@ -0,0 +1,20 @@
+// src/types/vue-router.d.ts
+import 'vue-router'
+
+import type {AntDesignOutlinedIconType} from '@ant-design/icons-vue/lib/icons/AntDesignOutlined'
+
+/**
+ * @description Extend the types of router meta
+ */
+
+declare module 'vue-router' {
+  interface RouteMeta {
+    name: (() => string)
+    icon?: AntDesignOutlinedIconType
+    hiddenInSidebar?: boolean
+    hideChildren?: boolean
+    noAuth?: boolean
+    status_code?: number
+    error?: () => string
+  }
+}

+ 1 - 0
app/src/types.d.ts

@@ -0,0 +1 @@
+export type CheckedType = boolean | string | number

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.18","build_id":123,"total_build":327}
+{"version":"2.0.0-beta.18","build_id":126,"total_build":330}

+ 109 - 0
app/src/views/certificate/ACMEUser.vue

@@ -0,0 +1,109 @@
+<script setup lang="tsx">
+import { Tag, message } from 'ant-design-vue'
+import type { Column } from '@/components/StdDesign/types'
+import { StdCurd } from '@/components/StdDesign/StdDataDisplay'
+import type { AcmeUser } from '@/api/acme_user'
+import acme_user from '@/api/acme_user'
+import { input } from '@/components/StdDesign/StdDataEntry'
+import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+
+const columns: Column[] = [
+  {
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sortable: true,
+    pithy: true,
+    edit: {
+      type: input,
+    },
+  }, {
+    title: () => $gettext('Email'),
+    dataIndex: 'email',
+    sortable: true,
+    pithy: true,
+    edit: {
+      type: input,
+    },
+  }, {
+    title: () => $gettext('CA Dir'),
+    dataIndex: 'ca_dir',
+    sortable: true,
+    pithy: true,
+    edit: {
+      type: input,
+      config: {
+        placeholder() {
+          return $gettext('If left black, the default CA Dir will be used.')
+        },
+      },
+    },
+  }, {
+    title: () => $gettext('Status'),
+    dataIndex: ['registration', 'body', 'status'],
+    customRender: (args: customRender) => {
+      if (args.text === 'valid')
+        return <Tag color="green">{$gettext('Valid')}</Tag>
+
+      return <Tag color="red">{$gettext('Invalid')}</Tag>
+    },
+    sortable: true,
+    pithy: true,
+  }, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'updated_at',
+    customRender: datetime,
+    sortable: true,
+    pithy: true,
+  }, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action',
+  },
+]
+
+function register(id: number, data: AcmeUser) {
+  acme_user.register(id).then(r => {
+    data.registration = r.registration
+    message.success($gettext('Register successfully'))
+  }).catch(e => {
+    message.error(e?.message ?? $gettext('Register failed'))
+  })
+}
+</script>
+
+<template>
+  <StdCurd
+    :title="$gettext('ACME User')"
+    :columns="columns"
+    :api="acme_user"
+  >
+    <template #edit="{ data }: {data: AcmeUser}">
+      <template v-if="data.id > 0 ">
+        <div class="mb-2">
+          <label>{{ $gettext('Registration Status') }}</label>
+        </div>
+        <template v-if="data?.registration?.body?.status === 'valid'">
+          <ATag color="green">
+            {{ $gettext('Valid') }}
+          </ATag>
+        </template>
+        <template v-else>
+          <ATag color="red">
+            {{ $gettext('Invalid') }}
+          </ATag>
+        </template>
+
+        <AButton
+          type="link"
+          @click="register(data.id, data)"
+        >
+          {{ $gettext('Register') }}
+        </AButton>
+      </template>
+    </template>
+  </StdCurd>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 5 - 9
app/src/views/certificate/Certificate.vue

@@ -1,21 +1,17 @@
 <script setup lang="tsx">
-import { useGettext } from 'vue3-gettext'
 import { Badge, Tag } from 'ant-design-vue'
-import { h, provide } from 'vue'
 import dayjs from 'dayjs'
 import { CloudUploadOutlined, SafetyCertificateOutlined } from '@ant-design/icons-vue'
 import { input } from '@/components/StdDesign/StdDataEntry'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import cert from '@/api/cert'
-import type { Column } from '@/components/StdDesign/types'
+import type { Column, JSXElements } from '@/components/StdDesign/types'
 import type { Cert } from '@/api/cert'
 import { AutoCertState } from '@/constants'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import WildcardCertificate from '@/views/certificate/WildcardCertificate.vue'
 
-const { $gettext } = useGettext()
-
 function notShowInAutoCert(record: Cert) {
   return record.auto_cert !== AutoCertState.Enable
 }
@@ -41,7 +37,7 @@ const columns: Column[] = [{
   title: () => $gettext('Type'),
   dataIndex: 'auto_cert',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
     const { text } = args
     const managed = $gettext('Managed Certificate')
     const general = $gettext('General Certificate')
@@ -68,7 +64,7 @@ const columns: Column[] = [{
     type: input,
     show: notShowInAutoCert,
   },
-  hidden: true,
+  hiddenInTable: true,
 }, {
   title: () => $gettext('SSL Certificate Key Path'),
   dataIndex: 'ssl_certificate_key_path',
@@ -76,12 +72,12 @@ const columns: Column[] = [{
     type: input,
     show: notShowInAutoCert,
   },
-  hidden: true,
+  hiddenInTable: true,
 }, {
   title: () => $gettext('Status'),
   dataIndex: 'certificate_info',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
 
     const text = args.text?.not_before && args.text?.not_after && !dayjs().isBefore(args.text?.not_before) && !dayjs().isAfter(args.text?.not_after)
 

+ 0 - 3
app/src/views/certificate/CertificateEditor.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type { Ref } from 'vue'
 import { message } from 'ant-design-vue'
 import { AutoCertState } from '@/constants'
@@ -11,8 +10,6 @@ import cert from '@/api/cert'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import RenewCert from '@/views/certificate/RenewCert.vue'
 
-const { $gettext } = useGettext()
-
 const route = useRoute()
 
 const id = computed(() => {

+ 0 - 2
app/src/views/certificate/DNSChallenge.vue

@@ -1,11 +1,9 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type { SelectProps } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import type { DNSProvider } from '@/api/auto_cert'
 import auto_cert from '@/api/auto_cert'
 
-const { $gettext } = useGettext()
 const providers = ref([]) as Ref<DNSProvider[]>
 
 // This data is provided by the Top StdCurd component,

+ 0 - 3
app/src/views/certificate/DNSCredential.vue

@@ -1,5 +1,4 @@
 <script setup lang="tsx">
-import { useGettext } from 'vue3-gettext'
 import DNSChallenge from './DNSChallenge.vue'
 import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import dns_credential from '@/api/dns_credential'
@@ -7,8 +6,6 @@ import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
 import { input } from '@/components/StdDesign/StdDataEntry'
 import type { Column } from '@/components/StdDesign/types'
 
-const { $gettext } = useGettext()
-
 const columns: Column[] = [{
   title: () => $gettext('Name'),
   dataIndex: 'name',

+ 0 - 3
app/src/views/certificate/RenewCert.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type { Ref } from 'vue'
 import { message } from 'ant-design-vue'
 import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
@@ -9,8 +8,6 @@ const emit = defineEmits<{
   renewed: [void]
 }>()
 
-const { $gettext } = useGettext()
-
 const modalVisible = ref(false)
 const modalClosable = ref(true)
 

+ 0 - 2
app/src/views/certificate/WildcardCertificate.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
 import { message } from 'ant-design-vue'
-import { useGettext } from 'vue3-gettext'
 import type { Cert } from '@/api/cert'
 import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
 import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
@@ -10,7 +9,6 @@ const emit = defineEmits<{
   issued: [void]
 }>()
 
-const { $gettext } = useGettext()
 const step = ref(0)
 const visible = ref(false)
 const data = ref({}) as Ref<Cert>

+ 0 - 3
app/src/views/config/Config.vue

@@ -2,15 +2,12 @@
 import { computed, ref, watch } from 'vue'
 import { useRoute } from 'vue-router'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
-import gettext from '@/gettext'
 import config from '@/api/config'
 import configColumns from '@/views/config/config'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import router from '@/routes'
 import InspectConfig from '@/views/config/InspectConfig.vue'
 
-const { $gettext } = gettext
-
 const api = config
 
 const table = ref(null)

+ 3 - 4
app/src/views/config/ConfigEdit.vue

@@ -4,7 +4,7 @@ import { message } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import { formatDateTime } from '@/lib/helper'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
-import gettext from '@/gettext'
+
 import config from '@/api/config'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 import ngx from '@/api/ngx'
@@ -12,7 +12,6 @@ import InspectConfig from '@/views/config/InspectConfig.vue'
 import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
 import type { ChatComplicationMessage } from '@/api/openai'
 
-const { $gettext, interpolate } = gettext
 const route = useRoute()
 
 const inspect_config = ref()
@@ -56,7 +55,7 @@ function save() {
     configText.value = r.content
     message.success($gettext('Saved successfully'))
   }).catch(r => {
-    message.error(interpolate($gettext('Save error %{msg}'), { msg: r.message ?? '' }))
+    message.error($gettext('Save error %{msg}', { msg: r.message ?? '' }))
   }).finally(() => {
     inspect_config.value.test()
   })
@@ -67,7 +66,7 @@ function format_code() {
     configText.value = r.content
     message.success($gettext('Format successfully'))
   }).catch(r => {
-    message.error(interpolate($gettext('Format error %{msg}'), { msg: r.message ?? '' }))
+    message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
   })
 }
 

+ 0 - 3
app/src/views/config/InspectConfig.vue

@@ -1,10 +1,7 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import ngx from '@/api/ngx'
 import { logLevel } from '@/views/config/constants'
 
-const { $gettext } = useGettext()
-
 const data = ref({
   level: 0,
   message: '',

+ 2 - 4
app/src/views/config/config.ts

@@ -1,9 +1,7 @@
 import { h } from 'vue'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
-import gettext from '@/gettext'
-
-const { $gettext } = gettext
+import type { JSXElements } from '@/components/StdDesign/types'
 
 const configColumns = [{
   title: () => $gettext('Name'),
@@ -14,7 +12,7 @@ const configColumns = [{
   title: () => $gettext('Type'),
   dataIndex: 'is_dir',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
     const { text } = args
     if (text === true || text > 0)
       template.push($gettext('Directory'))

+ 1 - 4
app/src/views/dashboard/Environments.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import Icon, { LinkOutlined, SendOutlined, ThunderboltOutlined } from '@ant-design/icons-vue'
 import type ReconnectingWebSocket from 'reconnecting-websocket'
 import type { Ref } from 'vue'
@@ -7,13 +6,11 @@ import { useSettingsStore } from '@/pinia'
 import type { Node } from '@/api/environment'
 import environment from '@/api/environment'
 import logo from '@/assets/img/logo.png'
-import pulse from '@/assets/svg/pulse.svg'
+import pulse from '@/assets/svg/pulse.svg?component'
 import { formatDateTime } from '@/lib/helper'
 import NodeAnalyticItem from '@/views/dashboard/components/NodeAnalyticItem.vue'
 import analytic from '@/api/analytic'
 
-const { $gettext } = useGettext()
-
 const data = ref([]) as Ref<Node[]>
 
 const node_map = computed(() => {

+ 0 - 3
app/src/views/dashboard/ServerAnalytic.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type ReconnectingWebSocket from 'reconnecting-websocket'
 import AreaChart from '@/components/Chart/AreaChart.vue'
 import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
@@ -8,8 +7,6 @@ import analytic from '@/api/analytic'
 import { bytesToSize } from '@/lib/helper'
 import type { Series } from '@/components/Chart/types'
 
-const { $gettext } = useGettext()
-
 let websocket: ReconnectingWebSocket | WebSocket
 
 const host: HostInfoStat = reactive({

+ 2 - 2
app/src/views/dashboard/components/NodeAnalyticItem.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import Icon, { ArrowDownOutlined, ArrowUpOutlined, DatabaseOutlined, LineChartOutlined } from '@ant-design/icons-vue'
-import cpu from '@/assets/svg/cpu.svg'
-import memory from '@/assets/svg/memory.svg'
+import cpu from '@/assets/svg/cpu.svg?component'
+import memory from '@/assets/svg/memory.svg?component'
 import { bytesToSize } from '@/lib/helper'
 import UsageProgressLine from '@/components/Chart/UsageProgressLine.vue'
 

+ 0 - 3
app/src/views/domain/DomainAdd.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { message } from 'ant-design-vue'
 import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor.vue'
 import LocationEditor from '@/views/domain/ngx_conf/LocationEditor.vue'
@@ -8,8 +7,6 @@ import domain from '@/api/domain'
 import type { NgxConfig } from '@/api/ngx'
 import ngx from '@/api/ngx'
 
-const { $gettext } = useGettext()
-
 const ngx_config: NgxConfig = reactive({
   name: '',
   servers: [{

+ 5 - 8
app/src/views/domain/DomainEdit.vue

@@ -1,7 +1,5 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { message } from 'ant-design-vue'
-import type { Ref } from 'vue'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 
@@ -14,8 +12,7 @@ import config from '@/api/config'
 import RightSettings from '@/views/domain/components/RightSettings.vue'
 import type { CertificateInfo } from '@/api/cert'
 import type { ChatComplicationMessage } from '@/api/openai'
-
-const { $gettext, interpolate } = useGettext()
+import type { CheckedType } from '@/types'
 
 const route = useRoute()
 const router = useRouter()
@@ -99,9 +96,9 @@ function handle_parse_error(e: { error?: string; message: string }) {
   })
 }
 
-function on_mode_change(advanced: boolean) {
-  domain.advance_mode(name.value, { advanced }).then(() => {
-    advance_mode.value = advanced
+function on_mode_change(advanced: CheckedType) {
+  domain.advance_mode(name.value, { advanced: advanced as boolean }).then(() => {
+    advance_mode.value = advanced as boolean
     if (advanced) {
       build_config()
     }
@@ -171,7 +168,7 @@ provide('data', data)
     >
       <ACard :bordered="false">
         <template #title>
-          <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), { n: name }) }}</span>
+          <span style="margin-right: 10px">{{ $gettext('Edit %{n}', { n: name }) }}</span>
           <ATag
             v-if="enabled"
             color="blue"

+ 2 - 5
app/src/views/domain/DomainList.vue

@@ -1,5 +1,4 @@
 <script setup lang="tsx">
-import { useGettext } from 'vue3-gettext'
 import { Badge, message } from 'ant-design-vue'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
@@ -8,9 +7,7 @@ import domain from '@/api/domain'
 import { input } from '@/components/StdDesign/StdDataEntry'
 import SiteDuplicate from '@/views/domain/components/SiteDuplicate.vue'
 import InspectConfig from '@/views/config/InspectConfig.vue'
-import type { Column } from '@/components/StdDesign/types'
-
-const { $gettext } = useGettext()
+import type { Column, JSXElements } from '@/components/StdDesign/types'
 
 const columns: Column[] = [{
   title: () => $gettext('Name'),
@@ -25,7 +22,7 @@ const columns: Column[] = [{
   title: () => $gettext('Status'),
   dataIndex: 'enabled',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
     const { text } = args
     if (text === true || text > 0) {
       template.push(<Badge status="success"/>)

+ 0 - 2
app/src/views/domain/cert/Cert.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { computed } from 'vue'
-import { useGettext } from 'vue3-gettext'
 import CertInfo from '@/views/domain/cert/CertInfo.vue'
 import IssueCert from '@/views/domain/cert/IssueCert.vue'
 import ChangeCert from '@/views/domain/cert/ChangeCert.vue'
@@ -14,7 +13,6 @@ const props = defineProps<{
 }>()
 
 const emit = defineEmits(['callback', 'update:enabled'])
-const { $gettext } = useGettext()
 function callback() {
   emit('callback')
 }

+ 0 - 3
app/src/views/domain/cert/CertInfo.vue

@@ -1,15 +1,12 @@
 <script setup lang="ts">
 import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
 import dayjs from 'dayjs'
-import { useGettext } from 'vue3-gettext'
 import type { CertificateInfo } from '@/api/cert'
 
 defineProps<{
   cert?: CertificateInfo
 }>()
 
-const { $gettext } = useGettext()
-
 </script>
 
 <template>

+ 2 - 5
app/src/views/domain/cert/ChangeCert.vue

@@ -1,5 +1,4 @@
 <script setup lang="tsx">
-import { useGettext } from 'vue3-gettext'
 import { Badge } from 'ant-design-vue'
 import type { ComputedRef, Ref } from 'vue'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
@@ -8,9 +7,7 @@ import cert from '@/api/cert'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { input } from '@/components/StdDesign/StdDataEntry'
 import type { NgxDirective } from '@/api/ngx'
-import type { Column } from '@/components/StdDesign/types'
-
-const { $gettext } = useGettext()
+import type { Column, JSXElements } from '@/components/StdDesign/types'
 
 const current_server_directives = inject('current_server_directives') as ComputedRef<NgxDirective[]>
 const directivesMap = inject('directivesMap') as Ref<Record<string, NgxDirective[]>>
@@ -37,7 +34,7 @@ const columns: Column[] = [{
   title: () => $gettext('Auto Cert'),
   dataIndex: 'auto_cert',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
     const { text } = args
     if (text === true || text > 0) {
       template.push(<Badge status="success"/>)

+ 0 - 2
app/src/views/domain/cert/IssueCert.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import ObtainCert from '@/views/domain/cert/components/ObtainCert.vue'
 import type { NgxDirective } from '@/api/ngx'
 
@@ -12,7 +11,6 @@ const props = defineProps<Props>()
 
 const emit = defineEmits(['callback', 'update:enabled'])
 
-const { $gettext } = useGettext()
 const issuing_cert = ref(false)
 const obtain_cert = ref()
 const directivesMap = inject('directivesMap') as Ref<Record<string, NgxDirective[]>>

+ 0 - 3
app/src/views/domain/cert/components/AutoCertStepOne.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type { Ref } from 'vue'
 import type { DnsChallenge } from '@/api/auto_cert'
 import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
@@ -9,8 +8,6 @@ defineProps<{
   hideNote?: boolean
 }>()
 
-const { $gettext } = useGettext()
-
 const no_server_name = inject('no_server_name')
 
 // Provide by ObtainCert.vue

+ 3 - 4
app/src/views/domain/cert/components/DNSChallenge.vue

@@ -1,12 +1,11 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type { SelectProps } from 'ant-design-vue'
 import type { Ref } from 'vue'
+import type { SelectValue } from 'ant-design-vue/es/select'
 import type { DNSProvider } from '@/api/auto_cert'
 import auto_cert from '@/api/auto_cert'
 import dns_credential from '@/api/dns_credential'
 
-const { $gettext } = useGettext()
 const providers = ref([]) as Ref<DNSProvider[]>
 const credentials = ref<SelectProps['options']>([])
 
@@ -14,7 +13,7 @@ const credentials = ref<SelectProps['options']>([])
 // is the object that you are trying to modify it
 // we externalize the dns_credential_id to the parent component,
 // this is used to tell the backend which dns_credential to use
-const data = inject('data') as Ref<DNSProvider & { dns_credential_id: number | null }>
+const data = inject('data') as Ref<DNSProvider & { dns_credential_id: SelectValue }>
 
 const code = computed(() => {
   return data.value.code
@@ -41,7 +40,7 @@ watch(current, () => {
   data.value.code = current.value.code
   data.value.provider = current.value.name
   if (mounted.value)
-    data.value.dns_credential_id = null
+    data.value.dns_credential_id = undefined
 
   dns_credential.get_list({ provider: data.value.provider }).then(r => {
     r.data.forEach(v => {

+ 4 - 7
app/src/views/domain/cert/components/ObtainCert.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { Modal, message } from 'ant-design-vue'
 import type { ComputedRef, Ref } from 'vue'
 import domain from '@/api/domain'
@@ -12,8 +11,6 @@ import type { CertificateResult } from '@/api/cert'
 
 const emit = defineEmits(['update:auto_cert'])
 
-const { $gettext, interpolate } = useGettext()
-
 const modalVisible = ref(false)
 const step = ref(1)
 const directivesMap = inject('directivesMap') as Ref<Record<string, NgxDirective[]>>
@@ -66,16 +63,16 @@ function change_auto_cert(status: boolean) {
       challenge_method: data.value.challenge_method,
       dns_credential_id: data.value.dns_credential_id,
     }).then(() => {
-      message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), { name: name.value }))
+      message.success($gettext('Auto-renewal enabled for %{name}', { name: name.value }))
     }).catch(e => {
-      message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), { name: name.value }))
+      message.error(e.message ?? $gettext('Enable auto-renewal failed for %{name}', { name: name.value }))
     })
   }
   else {
     domain.remove_auto_cert(props.configName).then(() => {
-      message.success(interpolate($gettext('Auto-renewal disabled for %{name}'), { name: name.value }))
+      message.success($gettext('Auto-renewal disabled for %{name}', { name: name.value }))
     }).catch(e => {
-      message.error(e.message ?? interpolate($gettext('Disable auto-renewal failed for %{name}'), { name: name.value }))
+      message.error(e.message ?? $gettext('Disable auto-renewal failed for %{name}', { name: name.value }))
     })
   }
 }

+ 1 - 4
app/src/views/domain/cert/components/ObtainCertLive.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
-import { useGettext } from 'vue3-gettext'
 import websocket from '@/lib/websocket'
 import type { DnsChallenge } from '@/api/auto_cert'
 import Error from '@/views/other/Error.vue'
@@ -34,8 +33,6 @@ const modalVisible = computed({
   },
 })
 
-const { $gettext } = useGettext()
-
 const issuing_cert = inject('issuing_cert') as Ref<boolean>
 const data = inject('data') as Ref<DnsChallenge>
 
@@ -45,7 +42,7 @@ const progressStrokeColor = {
 }
 
 const progressPercent = ref(0)
-const progressStatus = ref('active')
+const progressStatus = ref('active') as Ref<'success' | 'active' | 'normal' | 'exception'>
 
 const logContainer = ref()
 

+ 0 - 3
app/src/views/domain/components/Deploy.vue

@@ -1,13 +1,10 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { InfoCircleOutlined } from '@ant-design/icons-vue'
 import { Modal, notification } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import domain from '@/api/domain'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 
-const { $gettext, $ngettext } = useGettext()
-
 const node_map = reactive({})
 const target = ref([])
 const overwrite = ref(false)

+ 4 - 4
app/src/views/domain/components/RightSettings.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { Modal, message } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import type { Site } from '@/api/domain'
@@ -10,15 +9,16 @@ import Deploy from '@/views/domain/components/Deploy.vue'
 import { useSettingsStore } from '@/pinia'
 import type { ChatComplicationMessage } from '@/api/openai'
 import type { NgxConfig } from '@/api/ngx'
+import type { CheckedType } from '@/types'
 
 const settings = useSettingsStore()
-const { $gettext } = useGettext()
+
 const configText = inject('configText') as Ref<string>
 const ngx_config = inject('ngx_config') as Ref<NgxConfig>
 const enabled = inject('enabled') as Ref<boolean>
 const name = inject('name') as Ref<string>
 const history_chatgpt_record = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
-const filename = inject('filename')
+const filename = inject('filename') as Ref<string | number | undefined>
 const data = inject('data') as Ref<Site>
 
 const [modal, ContextHolder] = Modal.useModal()
@@ -43,7 +43,7 @@ function disable() {
   })
 }
 
-function on_change_enabled(checked: boolean) {
+function on_change_enabled(checked: CheckedType) {
   modal.confirm({
     title: checked ? $gettext('Do you want to enable this site?') : $gettext('Do you want to disable this site?'),
     mask: false,

+ 3 - 5
app/src/views/domain/components/SiteDuplicate.vue

@@ -1,10 +1,10 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { Form, message, notification } from 'ant-design-vue'
-import gettext from '@/gettext'
+
 import domain from '@/api/domain'
 import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
 import { useSettingsStore } from '@/pinia'
+import gettext from '@/gettext'
 
 const props = defineProps<{
   visible: boolean
@@ -13,8 +13,6 @@ const props = defineProps<{
 
 const emit = defineEmits(['update:visible', 'duplicated'])
 
-const { $gettext } = useGettext()
-
 const settings = useSettingsStore()
 
 const show = computed({
@@ -127,7 +125,7 @@ watch(() => gettext.current, () => {
     v-model:open="show"
     :title="$gettext('Duplicate')"
     :confirm-loading="loading"
-    :mask="null"
+    :mask="false"
     @ok="onSubmit"
   >
     <AForm layout="vertical">

+ 0 - 3
app/src/views/domain/ngx_conf/LocationEditor.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { reactive, ref } from 'vue'
 import { DeleteOutlined, HolderOutlined } from '@ant-design/icons-vue'
 import Draggable from 'vuedraggable'
@@ -14,8 +13,6 @@ const props = defineProps<{
 
 const ngx_config = inject('ngx_config') as NgxConfig
 
-const { $gettext } = useGettext()
-
 const location = reactive({
   comments: '',
   path: '',

+ 0 - 2
app/src/views/domain/ngx_conf/LogEntry.vue

@@ -2,7 +2,6 @@
 import { FileExclamationOutlined, FileTextOutlined } from '@ant-design/icons-vue'
 import { computed, ref } from 'vue'
 import { useRouter } from 'vue-router'
-import { useGettext } from 'vue3-gettext'
 import type { NgxConfig } from '@/api/ngx'
 
 const props = defineProps<{
@@ -11,7 +10,6 @@ const props = defineProps<{
   name?: string
 }>()
 
-const { $gettext } = useGettext()
 const accessIdx = ref()
 const errorIdx = ref()
 

+ 3 - 5
app/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { Modal } from 'ant-design-vue'
 import type { ComputedRef } from 'vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
@@ -8,6 +7,7 @@ import type { NgxConfig, NgxDirective } from '@/api/ngx'
 import type { CertificateInfo } from '@/api/cert'
 import NgxServer from '@/views/domain/ngx_conf/NgxServer.vue'
 import NgxUpstream from '@/views/domain/ngx_conf/NgxUpstream.vue'
+import type { CheckedType } from '@/types'
 
 const props = withDefaults(defineProps<{
   autoCert?: boolean
@@ -22,8 +22,6 @@ const props = withDefaults(defineProps<{
 
 const emit = defineEmits(['callback', 'update:autoCert'])
 
-const { $gettext } = useGettext()
-
 const save_config = inject('save_config') as () => Promise<void>
 
 const [modal, ContextHolder] = Modal.useModal()
@@ -40,7 +38,7 @@ onMounted(() => {
 
 const ngx_config = inject('ngx_config') as NgxConfig
 
-function confirm_change_tls(status: boolean) {
+function confirm_change_tls(status: CheckedType) {
   modal.confirm({
     title: $gettext('Do you want to enable TLS?'),
     content: $gettext('To make sure the certification auto-renewal can work normally, '
@@ -88,7 +86,7 @@ const directivesMap: ComputedRef<Record<string, NgxDirective[]>> = computed(() =
 })
 
 // eslint-disable-next-line sonarjs/cognitive-complexity
-function change_tls(status: boolean) {
+function change_tls(status: CheckedType) {
   if (status) {
     // deep copy servers[0] to servers[1]
     const server = JSON.parse(JSON.stringify(ngx_config.servers[0]))

+ 0 - 3
app/src/views/domain/ngx_conf/NgxServer.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 
 import { MoreOutlined, PlusOutlined } from '@ant-design/icons-vue'
-import { useGettext } from 'vue3-gettext'
 import type { ComputedRef, Ref } from 'vue'
 import { Modal } from 'ant-design-vue'
 import LogEntry from '@/views/domain/ngx_conf/LogEntry.vue'
@@ -24,8 +23,6 @@ withDefaults(defineProps<{
 
 const emit = defineEmits(['callback'])
 
-const { $gettext } = useGettext()
-
 const [modal, ContextHolder] = Modal.useModal()
 
 const current_server_index = inject('current_server_index') as Ref<number>

+ 0 - 3
app/src/views/domain/ngx_conf/NgxUpstream.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { MoreOutlined, PlusOutlined } from '@ant-design/icons-vue'
-import { useGettext } from 'vue3-gettext'
 import { Modal } from 'ant-design-vue'
 import _ from 'lodash'
 import type { NgxConfig, NgxDirective } from '@/api/ngx'
@@ -8,8 +7,6 @@ import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor.v
 import type { UpstreamStatus } from '@/api/upstream'
 import upstream from '@/api/upstream'
 
-const { $gettext } = useGettext()
-
 const [modal, ContextHolder] = Modal.useModal()
 
 const ngx_config = inject('ngx_config') as NgxConfig

+ 0 - 2
app/src/views/domain/ngx_conf/config_template/ConfigTemplate.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { storeToRefs } from 'pinia'
 import type { Ref } from 'vue'
 import type { Template } from '@/api/template'
@@ -16,7 +15,6 @@ const props = defineProps<{
   currentServerIndex: number
 }>()
 
-const { $gettext } = useGettext()
 const { language } = storeToRefs(useSettingsStore())
 const ngx_config = inject('ngx_config') as NgxConfig
 const blocks = ref([])

+ 0 - 3
app/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { type ComputedRef, reactive, ref } from 'vue'
-import { useGettext } from 'vue3-gettext'
 import { DeleteOutlined } from '@ant-design/icons-vue'
 import CodeEditor from '@/components/CodeEditor'
 import type { NgxDirective } from '@/api/ngx'
@@ -11,8 +10,6 @@ const props = defineProps<{
 
 const emit = defineEmits(['save'])
 
-const { $gettext } = useGettext()
-
 const ngx_directives = inject('ngx_directives') as ComputedRef<NgxDirective[]>
 const directive = reactive({ directive: '', params: '' })
 const adding = ref(false)

+ 0 - 2
app/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import Draggable from 'vuedraggable'
 import type { ComputedRef } from 'vue'
 import DirectiveAdd from './DirectiveAdd.vue'
@@ -11,7 +10,6 @@ defineProps<{
   context?: string
 }>()
 
-const { $gettext } = useGettext()
 const current_idx = ref(-1)
 
 const ngx_directives = inject('ngx_directives') as ComputedRef<NgxDirective[]>

+ 1 - 4
app/src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import { DeleteOutlined, HolderOutlined } from '@ant-design/icons-vue'
 
-import { useGettext } from 'vue3-gettext'
 import { type ComputedRef, ref, watch } from 'vue'
 import { message } from 'ant-design-vue'
 import config from '@/api/config'
@@ -14,8 +13,6 @@ const props = defineProps<{
   context?: string
 }>()
 
-const { $gettext, interpolate } = useGettext()
-
 const ngx_directives = inject('ngx_directives') as ComputedRef<NgxDirective[]>
 
 function remove(index: number) {
@@ -41,7 +38,7 @@ function save() {
     content.value = r.content
     message.success($gettext('Saved successfully'))
   }).catch(r => {
-    message.error(interpolate($gettext('Save error %{msg}'), { msg: r.message ?? '' }))
+    message.error($gettext('Save error %{msg}', { msg: r.message ?? '' }))
   })
 }
 

+ 3 - 6
app/src/views/environment/Environment.vue

@@ -1,5 +1,4 @@
 <script setup lang="tsx">
-import { useGettext } from 'vue3-gettext'
 import { h } from 'vue'
 import { Badge } from 'ant-design-vue'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
@@ -7,9 +6,7 @@ import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransfor
 import environment from '@/api/environment'
 import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
 import { input } from '@/components/StdDesign/StdDataEntry'
-import type { Column } from '@/components/StdDesign/types'
-
-const { $gettext } = useGettext()
+import type { Column, JSXElements } from '@/components/StdDesign/types'
 
 const columns: Column[] = [{
   title: () => $gettext('Name'),
@@ -36,7 +33,7 @@ const columns: Column[] = [{
   title: () => 'NodeSecret',
   dataIndex: 'token',
   sortable: true,
-  hidden: true,
+  hiddenInTable: true,
   edit: {
     type: input,
   },
@@ -78,7 +75,7 @@ const columns: Column[] = [{
   title: () => $gettext('Status'),
   dataIndex: 'status',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
     const { text } = args
     if (text === true || text > 0) {
       template.push(<Badge status="success"/>)

+ 0 - 2
app/src/views/nginx_log/NginxLog.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import type ReconnectingWebSocket from 'reconnecting-websocket'
 import { debounce } from 'lodash'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
@@ -7,7 +6,6 @@ import type { INginxLogData } from '@/api/nginx_log'
 import nginx_log from '@/api/nginx_log'
 import ws from '@/lib/websocket'
 
-const { $gettext } = useGettext()
 const logContainer = ref()
 let websocket: ReconnectingWebSocket | WebSocket
 const route = useRoute()

+ 1 - 4
app/src/views/notification/Notification.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { message } from 'ant-design-vue'
 import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
 import notification from '@/api/notification'
@@ -9,12 +8,10 @@ import { datetime, mask } from '@/components/StdDesign/StdDataDisplay/StdTableTr
 import { NotificationType } from '@/constants'
 import { useUserStore } from '@/pinia'
 
-const { $gettext } = useGettext()
-
 const columns: Column[] = [{
   title: () => $gettext('Type'),
   dataIndex: 'type',
-  customRender: (args: customRender) => mask(args, NotificationType),
+  customRender: mask(NotificationType),
   sortable: true,
   pithy: true,
 }, {

+ 0 - 3
app/src/views/other/Error.vue

@@ -1,7 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
-
-const { $gettext } = useGettext()
 
 const route = useRoute()
 

+ 5 - 7
app/src/views/other/Install.vue

@@ -4,12 +4,10 @@ import { reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { DatabaseOutlined, LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons-vue'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
-import gettext from '@/gettext'
+
 import install from '@/api/install'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
 
-const { $gettext, interpolate } = gettext
-
 const thisYear = new Date().getFullYear()
 const loading = ref(false)
 
@@ -49,10 +47,10 @@ const rulesRef = reactive({
   ],
   database: [
     {
-      message: () => interpolate(
-        $gettext('The filename cannot contain the following characters: %{c}'),
-        { c: '& &quot; ? < > # {} % ~ / \\' },
-      ),
+      message: () =>
+        $gettext('The filename cannot contain the following characters: %{c}',
+          { c: '& &quot; ? < > # {} % ~ / \\' },
+        ),
     },
   ],
 })

+ 2 - 2
app/src/views/other/Login.vue

@@ -1,12 +1,13 @@
 <script setup lang="ts">
 import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
 import { Form, message } from 'ant-design-vue'
-import gettext from '@/gettext'
+
 import { useUserStore } from '@/pinia'
 import auth from '@/api/auth'
 import install from '@/api/install'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
+import gettext from '@/gettext'
 
 const thisYear = new Date().getFullYear()
 
@@ -18,7 +19,6 @@ install.get_lock().then(async (r: { lock: boolean }) => {
     await router.push('/install')
 })
 
-const { $gettext } = gettext
 const loading = ref(false)
 
 const modelRef = reactive({

+ 0 - 2
app/src/views/preference/BasicSettings.vue

@@ -1,9 +1,7 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { inject } from 'vue'
 import type { Settings } from '@/views/preference/typedef'
 
-const { $gettext } = useGettext()
 const data: Settings = inject('data') as Settings
 const errors: Record<string, Record<string, string>> = inject('errors') as Record<string, Record<string, string>>
 </script>

+ 0 - 3
app/src/views/preference/LogrotateSettings.vue

@@ -1,10 +1,7 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { inject } from 'vue'
 import type { Settings } from '@/views/preference/typedef'
 
-const { $gettext } = useGettext()
-
 const data: Settings = inject('data')!
 </script>
 

+ 0 - 3
app/src/views/preference/NginxSettings.vue

@@ -1,10 +1,7 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { inject } from 'vue'
 import type { Settings } from '@/views/preference/typedef'
 
-const { $gettext } = useGettext()
-
 const data: Settings = inject('data')!
 const errors: Record<string, Record<string, string>> = inject('errors') as Record<string, Record<string, string>>
 </script>

+ 0 - 3
app/src/views/preference/OpenAISettings.vue

@@ -1,10 +1,7 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { inject } from 'vue'
 import type { Settings } from '@/views/preference/typedef'
 
-const { $gettext } = useGettext()
-
 const data: Settings = inject('data')!
 const errors: Record<string, Record<string, string>> = inject('errors') as Record<string, Record<string, string>>
 </script>

+ 0 - 3
app/src/views/preference/Preference.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { message } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
@@ -10,8 +9,6 @@ import NginxSettings from '@/views/preference/NginxSettings.vue'
 import type { Settings } from '@/views/preference/typedef'
 import LogrotateSettings from '@/views/preference/LogrotateSettings.vue'
 
-const { $gettext } = useGettext()
-
 const data = ref<Settings>({
   server: {
     http_host: '0.0.0.0',

+ 3 - 7
app/src/views/pty/Terminal.vue

@@ -1,16 +1,13 @@
 <script setup lang="ts">
-import 'xterm/css/xterm.css'
-import { Terminal } from 'xterm'
+import '@xterm/xterm/css/xterm.css'
+import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
 import { onMounted, onUnmounted } from 'vue'
 import _ from 'lodash'
-import { useGettext } from 'vue3-gettext'
 import ws from '@/lib/websocket'
 
-const { $gettext } = useGettext()
-
 let term: Terminal | null
-let ping: number
+let ping: NodeJS.Timeout
 
 const websocket = ws('/api/pty')
 
@@ -85,7 +82,6 @@ onUnmounted(() => {
   window.removeEventListener('resize', fit)
   clearInterval(ping)
   term?.dispose()
-  ping = 0
   websocket.close()
 })
 

+ 5 - 7
app/src/views/stream/StreamEdit.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { useGettext } from 'vue3-gettext'
 import { message } from 'ant-design-vue'
 import type { Ref } from 'vue'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
@@ -13,8 +12,7 @@ import RightSettings from '@/views/stream/components/RightSettings.vue'
 import type { ChatComplicationMessage } from '@/api/openai'
 import type { Stream } from '@/api/stream'
 import stream from '@/api/stream'
-
-const { $gettext, interpolate } = useGettext()
+import type { CheckedType } from '@/types'
 
 const route = useRoute()
 const router = useRouter()
@@ -90,9 +88,9 @@ function handle_parse_error(e: { error?: string; message: string }) {
   })
 }
 
-function on_mode_change(advanced: boolean) {
-  stream.advance_mode(name.value, { advanced }).then(() => {
-    advance_mode.value = advanced
+function on_mode_change(advanced: CheckedType) {
+  stream.advance_mode(name.value, { advanced: advanced as boolean }).then(() => {
+    advance_mode.value = advanced as boolean
     if (advanced) {
       build_config()
     }
@@ -162,7 +160,7 @@ provide('data', data)
     >
       <ACard :bordered="false">
         <template #title>
-          <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), { n: name }) }}</span>
+          <span style="margin-right: 10px">{{ $gettext('Edit %{n}', { n: name }) }}</span>
           <ATag
             v-if="enabled"
             color="blue"

+ 2 - 5
app/src/views/stream/StreamList.vue

@@ -1,5 +1,4 @@
 <script setup lang="tsx">
-import { useGettext } from 'vue3-gettext'
 import { Badge, message } from 'ant-design-vue'
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
@@ -7,11 +6,9 @@ import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransfor
 import stream from '@/api/stream'
 import { input } from '@/components/StdDesign/StdDataEntry'
 import InspectConfig from '@/views/config/InspectConfig.vue'
-import type { Column } from '@/components/StdDesign/types'
+import type { Column, JSXElements } from '@/components/StdDesign/types'
 import StreamDuplicate from '@/views/stream/components/StreamDuplicate.vue'
 
-const { $gettext } = useGettext()
-
 const columns: Column[] = [{
   title: () => $gettext('Name'),
   dataIndex: 'name',
@@ -25,7 +22,7 @@ const columns: Column[] = [{
   title: () => $gettext('Status'),
   dataIndex: 'enabled',
   customRender: (args: customRender) => {
-    const template = []
+    const template: JSXElements = []
     const { text } = args
     if (text === true || text > 0) {
       template.push(<Badge status="success"/>)

Some files were not shown because too many files changed in this diff