Explorar o código

refactor: upgrade ant design vue v3 to v4

0xJacky hai 1 ano
pai
achega
d84dbffb54
Modificáronse 100 ficheiros con 5209 adicións e 5823 borrados
  1. 4 0
      .dockerignore
  2. 4 1
      .editorconfig
  3. 10 2
      dev.Dockerfile
  4. 3 3
      frontend/.vscode/extensions.json
  5. 0 1
      frontend/.yarnrc.yml
  6. 5 3
      frontend/components.d.ts
  7. 3 3
      frontend/gettext.config.js
  8. 6 6
      frontend/i18n.json
  9. 9 9
      frontend/index.html
  10. 56 59
      frontend/package.json
  11. 21 18
      frontend/src/App.vue
  12. 3 3
      frontend/src/api/analytic.ts
  13. 22 22
      frontend/src/api/auth.ts
  14. 6 6
      frontend/src/api/auto_cert.ts
  15. 28 28
      frontend/src/api/curd.ts
  16. 22 22
      frontend/src/api/domain.ts
  17. 6 6
      frontend/src/api/install.ts
  18. 7 7
      frontend/src/api/nginx_log.ts
  19. 21 21
      frontend/src/api/ngx.ts
  20. 3 3
      frontend/src/api/openai.ts
  21. 6 6
      frontend/src/api/settings.ts
  22. 15 15
      frontend/src/api/template.ts
  23. 10 10
      frontend/src/api/upgrade.ts
  24. 8 1
      frontend/src/assets/svg/ChatGPT_logo.svg
  25. 0 0
      frontend/src/assets/svg/cpu.svg
  26. 8 1
      frontend/src/assets/svg/memory.svg
  27. 8 1
      frontend/src/assets/svg/pulse.svg
  28. 21 21
      frontend/src/components/Breadcrumb/Breadcrumb.vue
  29. 86 86
      frontend/src/components/Chart/AreaChart.vue
  30. 77 70
      frontend/src/components/Chart/RadialBarChart.vue
  31. 23 23
      frontend/src/components/Chart/UsageProgressLine.vue
  32. 206 206
      frontend/src/components/ChatGPT/ChatGPT.vue
  33. 11 11
      frontend/src/components/CodeEditor/CodeEditor.vue
  34. 49 49
      frontend/src/components/EnvIndicator/EnvIndicator.vue
  35. 37 37
      frontend/src/components/FooterToolbar/FooterToolBar.vue
  36. 25 25
      frontend/src/components/Logo/Logo.vue
  37. 68 68
      frontend/src/components/NginxControl/NginxControl.vue
  38. 28 28
      frontend/src/components/NodeSelector/NodeSelector.vue
  39. 143 143
      frontend/src/components/PageHeader/PageHeader.vue
  40. 17 17
      frontend/src/components/SetLanguage/SetLanguage.vue
  41. 38 38
      frontend/src/components/StdDataDisplay/StdBatchEdit.vue
  42. 135 135
      frontend/src/components/StdDataDisplay/StdCurd.vue
  43. 26 26
      frontend/src/components/StdDataDisplay/StdPagination.vue
  44. 421 421
      frontend/src/components/StdDataDisplay/StdTable.vue
  45. 17 17
      frontend/src/components/StdDataDisplay/StdTableTransformer.tsx
  46. 27 27
      frontend/src/components/StdDataEntry/StdDataEntry.tsx
  47. 18 18
      frontend/src/components/StdDataEntry/StdFormItem.vue
  48. 27 27
      frontend/src/components/StdDataEntry/components/StdPassword.vue
  49. 23 23
      frontend/src/components/StdDataEntry/components/StdSelect.vue
  50. 91 91
      frontend/src/components/StdDataEntry/components/StdSelector.vue
  51. 90 90
      frontend/src/components/StdDataEntry/index.tsx
  52. 5 5
      frontend/src/components/StdDataEntry/style.less
  53. 67 0
      frontend/src/components/SwitchAppearance/SwitchAppearance.vue
  54. 6 0
      frontend/src/components/SwitchAppearance/icons/VPIconMoon.vue
  55. 18 0
      frontend/src/components/SwitchAppearance/icons/VPIconSun.vue
  56. 92 0
      frontend/src/components/VPSwitch/VPSwitch.vue
  57. 0 9
      frontend/src/dark.less
  58. 4 4
      frontend/src/gettext.ts
  59. 27 27
      frontend/src/language/constants.ts
  60. 170 279
      frontend/src/language/en/app.po
  61. 178 289
      frontend/src/language/es/app.po
  62. 178 289
      frontend/src/language/fr_FR/app.po
  63. 171 316
      frontend/src/language/messages.pot
  64. 170 279
      frontend/src/language/ru_RU/app.po
  65. 0 0
      frontend/src/language/translations.json
  66. BIN=BIN
      frontend/src/language/zh_CN/app.mo
  67. 178 289
      frontend/src/language/zh_CN/app.po
  68. 178 289
      frontend/src/language/zh_TW/app.po
  69. 146 141
      frontend/src/layouts/BaseLayout.vue
  70. 2 2
      frontend/src/layouts/BaseRouterView.vue
  71. 8 8
      frontend/src/layouts/FooterLayout.vue
  72. 45 42
      frontend/src/layouts/HeaderLayout.vue
  73. 20 20
      frontend/src/layouts/Loading.vue
  74. 87 87
      frontend/src/layouts/SideBar.vue
  75. 57 57
      frontend/src/lib/helper/index.ts
  76. 49 49
      frontend/src/lib/http/index.ts
  77. 0 32
      frontend/src/lib/theme/index.ts
  78. 11 11
      frontend/src/lib/websocket/index.ts
  79. 0 1
      frontend/src/main.ts
  80. 2 2
      frontend/src/pinia/index.ts
  81. 29 28
      frontend/src/pinia/moudule/settings.ts
  82. 16 16
      frontend/src/pinia/moudule/user.ts
  83. 191 191
      frontend/src/routes/index.ts
  84. 0 1
      frontend/src/style.less
  85. 1 1
      frontend/src/version.json
  86. 102 102
      frontend/src/views/cert/Cert.vue
  87. 43 43
      frontend/src/views/cert/DNSChallenge.vue
  88. 37 37
      frontend/src/views/cert/DNSCredential.vue
  89. 25 25
      frontend/src/views/config/Config.vue
  90. 83 83
      frontend/src/views/config/ConfigEdit.vue
  91. 32 32
      frontend/src/views/config/InspectConfig.vue
  92. 26 26
      frontend/src/views/config/config.ts
  93. 8 8
      frontend/src/views/config/constants.ts
  94. 4 4
      frontend/src/views/dashboard/DashBoard.vue
  95. 82 82
      frontend/src/views/dashboard/Environments.vue
  96. 236 236
      frontend/src/views/dashboard/ServerAnalytic.vue
  97. 55 55
      frontend/src/views/dashboard/components/NodeAnalyticItem.vue
  98. 109 109
      frontend/src/views/domain/DomainAdd.vue
  99. 173 173
      frontend/src/views/domain/DomainEdit.vue
  100. 90 90
      frontend/src/views/domain/DomainList.vue

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
+.git
+frontend/node_modules
+.idea
+tmp

+ 4 - 1
.editorconfig

@@ -5,11 +5,14 @@ block_comment_start = /*
 block_comment_end = */
 charset = utf-8
 indent_style = space
-indent_size = 4
+indent_size = 2
 end_of_line = lf
 insert_final_newline = true
 trim_trailing_whitespace = true
 
+[*.go]
+indent_size = 4
+
 [README.md]
 indent_style = space
 indent_size = 4

+ 10 - 2
dev.Dockerfile

@@ -5,7 +5,7 @@ EXPOSE 80 443
 
 # COPY resources/development/sources.list /etc/apt/sources.list
 
-ENV GO_VERSION="1.21.0"
+ENV GO_VERSION="1.21.4"
 ENV GO_ARCH="linux-arm64"
 ENV GO_TAR="go${GO_VERSION}.${GO_ARCH}.tar.gz"
 ENV PATH="${PATH}:/usr/local/go/bin"
@@ -14,7 +14,15 @@ RUN set -x \
     # create nginx user/group first, to be consistent throughout docker variants
     && addgroup --system --gid 101 nginx \
     && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
-    && apt update && apt install -y wget nginx gcc curl
+    && apt update && apt install gcc curl gnupg2 ca-certificates lsb-release ubuntu-keyring wget -y \
+    && curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
+           | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null \
+    && echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
+       http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
+           | tee /etc/apt/sources.list.d/nginx.list
+
+RUN echo "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" | tee /etc/apt/preferences.d/99nginx \
+    && apt update && apt install nginx -y
 
 RUN wget https://go.dev/dl/${GO_TAR} && \
     rm -rf /usr/local/go && tar -C /usr/local -xzf ${GO_TAR} && rm -f ${GO_TAR}

+ 3 - 3
frontend/.vscode/extensions.json

@@ -1,5 +1,5 @@
 {
-    "recommendations": [
-        "Vue.volar"
-    ]
+  "recommendations": [
+    "Vue.volar"
+  ]
 }

+ 0 - 1
frontend/.yarnrc.yml

@@ -1 +0,0 @@
-nodeLinker: pnp

+ 5 - 3
frontend/components.d.ts

@@ -3,11 +3,9 @@
 // @ts-nocheck
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
-import '@vue/runtime-core'
-
 export {}
 
-declare module '@vue/runtime-core' {
+declare module 'vue' {
   export interface GlobalComponents {
     AAlert: typeof import('ant-design-vue/es')['Alert']
     AAvatar: typeof import('ant-design-vue/es')['Avatar']
@@ -87,5 +85,9 @@ declare module '@vue/runtime-core' {
     StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDataEntry/components/StdSelect.vue')['default']
     StdDataEntryComponentsStdSelector: typeof import('./src/components/StdDataEntry/components/StdSelector.vue')['default']
     StdDataEntryStdFormItem: typeof import('./src/components/StdDataEntry/StdFormItem.vue')['default']
+    SwitchAppearanceIconsVPIconMoon: typeof import('./src/components/SwitchAppearance/icons/VPIconMoon.vue')['default']
+    SwitchAppearanceIconsVPIconSun: typeof import('./src/components/SwitchAppearance/icons/VPIconSun.vue')['default']
+    SwitchAppearanceSwitchAppearance: typeof import('./src/components/SwitchAppearance/SwitchAppearance.vue')['default']
+    VPSwitchVPSwitch: typeof import('./src/components/VPSwitch/VPSwitch.vue')['default']
   }
 }

+ 3 - 3
frontend/gettext.config.js

@@ -1,7 +1,7 @@
 const i18n = require('./i18n.json')
 
 module.exports = {
-    output: {
-        locales: Object.keys(i18n),
-    },
+  output: {
+    locales: Object.keys(i18n),
+  },
 }

+ 6 - 6
frontend/i18n.json

@@ -1,8 +1,8 @@
 {
-    "en": "En",
-    "zh_CN": "简",
-    "zh_TW": "繁",
-    "fr_FR": "Fr",
-    "es": "Es",
-    "ru_RU": "Ru"
+  "en": "En",
+  "zh_CN": "简",
+  "zh_TW": "繁",
+  "fr_FR": "Fr",
+  "es": "Es",
+  "ru_RU": "Ru"
 }

+ 9 - 9
frontend/index.html

@@ -1,15 +1,15 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <meta charset="UTF-8"/>
-    <link href="/favicon.ico" rel="icon">
-    <meta content="width=device-width,initial-scale=1.0,user-scalable=0" name="viewport">
-    <style type="text/css">
-        #app {
-            height: 100%;
-        }
-    </style>
-    <title><%- title %></title>
+	<meta charset="UTF-8"/>
+	<link href="/favicon.ico" rel="icon">
+	<meta content="width=device-width,initial-scale=1.0,user-scalable=0" name="viewport">
+	<style type="text/css">
+            #app {
+                height: 100%;
+            }
+	</style>
+	<title><%- title %></title>
 </head>
 <body>
 <div id="app"></div>

+ 56 - 59
frontend/package.json

@@ -1,61 +1,58 @@
 {
-    "name": "nginx-ui-frontend-next",
-    "private": true,
-    "version": "2.0.0-beta.4",
-    "type": "commonjs",
-    "scripts": {
-        "dev": "vite",
-        "build": "vite build",
-        "preview": "vite preview",
-        "gettext:extract": "vue-gettext-extract",
-        "gettext:compile": "vue-gettext-compile"
-    },
-    "dependencies": {
-        "@ant-design/icons-vue": "^6.1.0",
-        "@formkit/auto-animate": "^0.8.0",
-        "@types/lodash": "^4.14.188",
-        "@types/marked": "^4.0.8",
-        "@types/nprogress": "^0.2.0",
-        "@types/sortablejs": "^1.15.0",
-        "@vue/reactivity": "^3.3.4",
-        "@vue/shared": "^3.3.4",
-        "ant-design-vue": "^3.2.17",
-        "apexcharts": "^3.36.3",
-        "axios": "^1.6.0",
-        "dayjs": "^1.11.7",
-        "highlight.js": "^11.7.0",
-        "lodash": "^4.17.21",
-        "marked": "^4.2.5",
-        "nprogress": "^0.2.0",
-        "pinia": "^2.0.28",
-        "pinia-plugin-persistedstate": "^3.0.2",
-        "reconnecting-websocket": "^4.4.0",
-        "sortablejs": "^1.15.0",
-        "vite-plugin-build-id": "^0.2.3",
-        "vue": "^3.2.47",
-        "vue-github-button": "https://github.com/0xJacky/vue-github-button",
-        "vue-router": "^4.1.6",
-        "vue3-ace-editor": "2.2.2",
-        "vue3-apexcharts": "^1.4.1",
-        "vue3-gettext": "^2.5.0-alpha.1",
-        "vuedraggable": "^4.1.0",
-        "xterm": "^5.1.0",
-        "xterm-addon-attach": "^0.8.0",
-        "xterm-addon-fit": "^0.7.0"
-    },
-    "devDependencies": {
-        "@vitejs/plugin-vue": "^4.2.1",
-        "@vitejs/plugin-vue-jsx": "^3.0.1",
-        "@vue/compiler-sfc": "^3.3.4",
-        "@zougt/vite-plugin-theme-preprocessor": "^1.4.8",
-        "ace-builds": "^1.30.0",
-        "less": "^4.1.3",
-        "typescript": "^5.0.4",
-        "unplugin-vue-components": "^0.24.1",
-        "vite": "^4.5.0",
-        "vite-plugin-html": "^3.2.0",
-        "vite-svg-loader": "^4.0.0",
-        "vue-tsc": "^1.6.1"
-    },
-    "packageManager": "yarn@3.6.4"
+  "name": "nginx-ui-frontend-next",
+  "private": true,
+  "version": "2.0.0-beta.4",
+  "type": "commonjs",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "gettext:extract": "vue-gettext-extract",
+    "gettext:compile": "vue-gettext-compile"
+  },
+  "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
+    "@formkit/auto-animate": "^0.8.0",
+    "@types/lodash": "^4.14.202",
+    "@types/nprogress": "^0.2.0",
+    "@types/sortablejs": "^1.15.0",
+    "@vue/reactivity": "^3.3.9",
+    "@vue/shared": "^3.3.9",
+    "ant-design-vue": "4.0.7",
+    "apexcharts": "^3.36.3",
+    "axios": "^1.6.2",
+    "dayjs": "^1.11.10",
+    "highlight.js": "^11.9.0",
+    "lodash": "^4.17.21",
+    "marked": "^10.0.0",
+    "nprogress": "^0.2.0",
+    "pinia": "^2.1.7",
+    "pinia-plugin-persistedstate": "^3.0.2",
+    "reconnecting-websocket": "^4.4.0",
+    "sortablejs": "^1.15.0",
+    "vite-plugin-build-id": "^0.2.3",
+    "vue": "^3.3.9",
+    "vue-github-button": "https://github.com/0xJacky/vue-github-button",
+    "vue-router": "^4.2.5",
+    "vue3-ace-editor": "2.2.4",
+    "vue3-apexcharts": "^1.4.4",
+    "vue3-gettext": "^3.0.0-beta.2",
+    "vuedraggable": "^4.1.0",
+    "xterm": "^5.3.0",
+    "xterm-addon-attach": "^0.9.0",
+    "xterm-addon-fit": "^0.8.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.5.0",
+    "@vitejs/plugin-vue-jsx": "^3.1.0",
+    "@vue/compiler-sfc": "^3.3.9",
+    "ace-builds": "^1.31.2",
+    "less": "^4.2.0",
+    "typescript": "^5.3.2",
+    "unplugin-vue-components": "^0.25.2",
+    "vite": "^5.0.2",
+    "vite-plugin-html": "^3.2.0",
+    "vite-svg-loader": "^5.1.0",
+    "vue-tsc": "^1.8.22"
+  }
 }

+ 21 - 18
frontend/src/App.vue

@@ -2,39 +2,42 @@
 // This starter template is using Vue 3 <script setup> SFCs
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
 import {useSettingsStore} from '@/pinia'
-import {dark_mode} from '@/lib/theme'
+import {computed, provide} from 'vue'
 
-let media = window.matchMedia('(prefers-color-scheme: dark)')
+const media = window.matchMedia('(prefers-color-scheme: dark)')
 
 const callback = (media: { matches: any; }) => {
-    const settings = useSettingsStore()
-    if (settings.preference_theme === 'auto') {
-        if (media.matches) {
-            dark_mode(true)
-            settings.set_theme('dark')
-        } else {
-            dark_mode(false)
-            settings.set_theme('auto')
-        }
+  const settings = useSettingsStore()
+  if (settings.preference_theme === 'auto') {
+    if (media.matches) {
+      settings.set_theme('dark')
     } else {
-        dark_mode(settings.preference_theme === 'dark')
+      settings.set_theme('light')
     }
+  } else {
+    settings.set_theme(settings.preference_theme)
+  }
 }
 
 callback(media)
 
-if (typeof media.addEventListener === 'function') {
-    media.addEventListener('change', callback)
-} else if (typeof media.addListener === 'function') {
-    media.addListener(callback)
-}
+const devicePrefersTheme = computed(() => {
+  return media.matches ? 'dark' : 'light'
+})
+
+provide('devicePrefersTheme', devicePrefersTheme)
 
+media.addEventListener('change', callback)
 </script>
 
 <template>
-    <router-view/>
+  <router-view/>
 </template>
 
+<style lang="less">
+@import "ant-design-vue/dist/reset.css";
+</style>
+
 <style lang="less" scoped>
 
 </style>

+ 3 - 3
frontend/src/api/analytic.ts

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

+ 22 - 22
frontend/src/api/auth.ts

@@ -5,28 +5,28 @@ const user = useUserStore()
 const {login, logout} = user
 
 const auth = {
-    async login(name: string, password: string) {
-        return http.post('/login', {
-            name: name,
-            password: password
-        }).then(r => {
-            login(r.token)
-        })
-    },
-    async casdoorLogin(code: string, state: string) {
-        await http.post("/casdoor_callback", {
-            code: code,
-            state: state,
-        })
-        .then((r) => {
-            login(r.token)
-        })
-    },
-    logout() {
-        return http.delete('/logout').then(async () => {
-            logout()
-        })
-    }
+  async login(name: string, password: string) {
+    return http.post('/login', {
+      name: name,
+      password: password
+    }).then(r => {
+      login(r.token)
+    })
+  },
+  async casdoorLogin(code: string, state: string) {
+    await http.post('/casdoor_callback', {
+      code: code,
+      state: state
+    })
+      .then((r) => {
+        login(r.token)
+      })
+  },
+  logout() {
+    return http.delete('/logout').then(async () => {
+      logout()
+    })
+  }
 }
 
 export default auth

+ 6 - 6
frontend/src/api/auto_cert.ts

@@ -1,13 +1,13 @@
 import http from '@/lib/http'
 
 const auto_cert = {
-    get_dns_providers() {
-        return http.get('/auto_cert/dns/providers')
-    },
+  get_dns_providers() {
+    return http.get('/auto_cert/dns/providers')
+  },
 
-    get_dns_provider(code: string) {
-        return http.get('/auto_cert/dns/provider/' + code)
-    }
+  get_dns_provider(code: string) {
+    return http.get('/auto_cert/dns/provider/' + code)
+  }
 }
 
 export default auto_cert

+ 28 - 28
frontend/src/api/curd.ts

@@ -1,34 +1,34 @@
 import http from '@/lib/http'
 
 class Curd {
-    protected readonly baseUrl: string
-    protected readonly plural: string
-
-    get_list = this._get_list.bind(this)
-    get = this._get.bind(this)
-    save = this._save.bind(this)
-    destroy = this._destroy.bind(this)
-
-    constructor(baseUrl: string, plural: string | null = null) {
-        this.baseUrl = baseUrl
-        this.plural = plural ?? this.baseUrl + 's'
-    }
-
-    _get_list(params: any = null) {
-        return http.get(this.plural, {params: params})
-    }
-
-    _get(id: any = null) {
-        return http.get(this.baseUrl + (id ? '/' + id : ''))
-    }
-
-    _save(id: any = null, data: any, config: any = undefined) {
-        return http.post(this.baseUrl + (id ? '/' + id : ''), data, config)
-    }
-
-    _destroy(id: any = null) {
-        return http.delete(this.baseUrl + '/' + id)
-    }
+  protected readonly baseUrl: string
+  protected readonly plural: string
+
+  get_list = this._get_list.bind(this)
+  get = this._get.bind(this)
+  save = this._save.bind(this)
+  destroy = this._destroy.bind(this)
+
+  constructor(baseUrl: string, plural: string | null = null) {
+    this.baseUrl = baseUrl
+    this.plural = plural ?? this.baseUrl + 's'
+  }
+
+  _get_list(params: any = null) {
+    return http.get(this.plural, {params: params})
+  }
+
+  _get(id: any = null) {
+    return http.get(this.baseUrl + (id ? '/' + id : ''))
+  }
+
+  _save(id: any = null, data: any, config: any = undefined) {
+    return http.post(this.baseUrl + (id ? '/' + id : ''), data, config)
+  }
+
+  _destroy(id: any = null) {
+    return http.delete(this.baseUrl + '/' + id)
+  }
 }
 
 export default Curd

+ 22 - 22
frontend/src/api/domain.ts

@@ -1,35 +1,35 @@
 import Curd from '@/api/curd'
 import http from '@/lib/http'
-import {AxiosRequestConfig} from "axios/index";
+import {AxiosRequestConfig} from 'axios/index'
 
 class Domain extends Curd {
-    enable(name: string, config: AxiosRequestConfig) {
-        return http.post(this.baseUrl + '/' + name + '/enable', undefined, config)
-    }
+  enable(name: string, config: AxiosRequestConfig) {
+    return http.post(this.baseUrl + '/' + name + '/enable', undefined, config)
+  }
 
-    disable(name: string) {
-        return http.post(this.baseUrl + '/' + name + '/disable')
-    }
+  disable(name: string) {
+    return http.post(this.baseUrl + '/' + name + '/disable')
+  }
 
-    get_template() {
-        return http.get('template')
-    }
+  get_template() {
+    return http.get('template')
+  }
 
-    add_auto_cert(domain: string, data: any) {
-        return http.post('auto_cert/' + domain, data)
-    }
+  add_auto_cert(domain: string, data: any) {
+    return http.post('auto_cert/' + domain, data)
+  }
 
-    remove_auto_cert(domain: string) {
-        return http.delete('auto_cert/' + domain)
-    }
+  remove_auto_cert(domain: string) {
+    return http.delete('auto_cert/' + domain)
+  }
 
-    duplicate(name: string, data: any) {
-        return http.post(this.baseUrl + '/' + name + '/duplicate', data)
-    }
+  duplicate(name: string, data: any) {
+    return http.post(this.baseUrl + '/' + name + '/duplicate', data)
+  }
 
-    advance_mode(name: string, data: any) {
-        return http.post(this.baseUrl + '/' + name + '/advance', data)
-    }
+  advance_mode(name: string, data: any) {
+    return http.post(this.baseUrl + '/' + name + '/advance', data)
+  }
 }
 
 const domain = new Domain('/domain')

+ 6 - 6
frontend/src/api/install.ts

@@ -1,12 +1,12 @@
 import http from '@/lib/http'
 
 const install = {
-    get_lock() {
-        return http.get('/install')
-    },
-    install_nginx_ui(data: any) {
-        return http.post('/install', data)
-    }
+  get_lock() {
+    return http.get('/install')
+  },
+  install_nginx_ui(data: any) {
+    return http.post('/install', data)
+  }
 }
 
 export default install

+ 7 - 7
frontend/src/api/nginx_log.ts

@@ -1,15 +1,15 @@
 import http from '@/lib/http'
 
 export interface INginxLogData {
-    type: string
-    conf_name: string
-    server_idx: number
-    directive_idx: number
+  type: string
+  conf_name: string
+  server_idx: number
+  directive_idx: number
 }
 
 const nginx_log = {
-    page(page = 0, data: INginxLogData) {
-        return http.post('/nginx_log?page=' + page, data)
-    }
+  page(page = 0, data: INginxLogData) {
+    return http.post('/nginx_log?page=' + page, data)
+  }
 }
 export default nginx_log

+ 21 - 21
frontend/src/api/ngx.ts

@@ -1,33 +1,33 @@
 import http from '@/lib/http'
 
 const ngx = {
-    build_config(ngxConfig: any) {
-        return http.post('/ngx/build_config', ngxConfig)
-    },
+  build_config(ngxConfig: any) {
+    return http.post('/ngx/build_config', ngxConfig)
+  },
 
-    tokenize_config(content: string) {
-        return http.post('/ngx/tokenize_config', {content})
-    },
+  tokenize_config(content: string) {
+    return http.post('/ngx/tokenize_config', {content})
+  },
 
-    format_code(content: string) {
-        return http.post('/ngx/format_code', {content})
-    },
+  format_code(content: string) {
+    return http.post('/ngx/format_code', {content})
+  },
 
-    status() {
-        return http.get('/nginx/status')
-    },
+  status() {
+    return http.get('/nginx/status')
+  },
 
-    reload() {
-        return http.post('/nginx/reload')
-    },
+  reload() {
+    return http.post('/nginx/reload')
+  },
 
-    restart() {
-        return http.post('/nginx/restart')
-    },
+  restart() {
+    return http.post('/nginx/restart')
+  },
 
-    test() {
-        return http.post('/nginx/test')
-    }
+  test() {
+    return http.post('/nginx/test')
+  }
 }
 
 export default ngx

+ 3 - 3
frontend/src/api/openai.ts

@@ -1,9 +1,9 @@
 import http from '@/lib/http'
 
 const openai = {
-    store_record(data: any) {
-        return http.post('/chat_gpt_record', data)
-    }
+  store_record(data: any) {
+    return http.post('/chat_gpt_record', data)
+  }
 }
 
 export default openai

+ 6 - 6
frontend/src/api/settings.ts

@@ -1,12 +1,12 @@
 import http from '@/lib/http'
 
 const settings = {
-    get() {
-        return http.get('/settings')
-    },
-    save(data: any) {
-        return http.post('/settings', data)
-    }
+  get() {
+    return http.get('/settings')
+  },
+  save(data: any) {
+    return http.post('/settings', data)
+  }
 }
 
 export default settings

+ 15 - 15
frontend/src/api/template.ts

@@ -2,25 +2,25 @@ import Curd from '@/api/curd'
 import http from '@/lib/http'
 
 class Template extends Curd {
-    get_config_list() {
-        return http.get('template/configs')
-    }
+  get_config_list() {
+    return http.get('template/configs')
+  }
 
-    get_block_list() {
-        return http.get('template/blocks')
-    }
+  get_block_list() {
+    return http.get('template/blocks')
+  }
 
-    get_config(name: string) {
-        return http.get('template/config/' + name)
-    }
+  get_config(name: string) {
+    return http.get('template/config/' + name)
+  }
 
-    get_block(name: string) {
-        return http.get('template/block/' + name)
-    }
+  get_block(name: string) {
+    return http.get('template/block/' + name)
+  }
 
-    build_block(name: string, data: any) {
-        return http.post('template/block/' + name, data)
-    }
+  build_block(name: string, data: any) {
+    return http.post('template/block/' + name, data)
+  }
 
 }
 

+ 10 - 10
frontend/src/api/upgrade.ts

@@ -1,16 +1,16 @@
 import http from '@/lib/http'
 
 const upgrade = {
-    get_latest_release(channel: string) {
-        return http.get('/upgrade/release', {
-            params: {
-                channel
-            }
-        })
-    },
-    current_version() {
-        return http.get('/upgrade/current')
-    }
+  get_latest_release(channel: string) {
+    return http.get('/upgrade/release', {
+      params: {
+        channel
+      }
+    })
+  },
+  current_version() {
+    return http.get('/upgrade/current')
+  }
 }
 
 export default upgrade

+ 8 - 1
frontend/src/assets/svg/ChatGPT_logo.svg

@@ -1 +1,8 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2406 2406"><path d="M1 578.4C1 259.5 259.5 1 578.4 1h1249.1c319 0 577.5 258.5 577.5 577.4V2406H578.4C259.5 2406 1 2147.5 1 1828.6V578.4z" fill="#74aa9c"/><path d="M1107.3 299.1c-198 0-373.9 127.3-435.2 315.3C544.8 640.6 434.9 720.2 370.5 833c-99.3 171.4-76.6 386.9 56.4 533.8-41.1 123.1-27 257.7 38.6 369.2 98.7 172 297.3 260.2 491.6 219.2 86.1 97 209.8 152.3 339.6 151.8 198 0 373.9-127.3 435.3-315.3 127.5-26.3 237.2-105.9 301-218.5 99.9-171.4 77.2-386.9-55.8-533.9v-.6c41.1-123.1 27-257.8-38.6-369.8-98.7-171.4-297.3-259.6-491-218.6-86.6-96.8-210.5-151.8-340.3-151.2zm0 117.5-.6.6c79.7 0 156.3 27.5 217.6 78.4-2.5 1.2-7.4 4.3-11 6.1L952.8 709.3c-18.4 10.4-29.4 30-29.4 51.4V1248l-155.1-89.4V755.8c-.1-187.1 151.6-338.9 339-339.2zm434.2 141.9c121.6-.2 234 64.5 294.7 169.8 39.2 68.6 53.9 148.8 40.4 226.5-2.5-1.8-7.3-4.3-10.4-6.1l-360.4-208.2c-18.4-10.4-41-10.4-59.4 0L1024 984.2V805.4L1372.7 604c51.3-29.7 109.5-45.4 168.8-45.5zM650 743.5v427.9c0 21.4 11 40.4 29.4 51.4l421.7 243-155.7 90L597.2 1355c-162-93.8-217.4-300.9-123.8-462.8C513.1 823.6 575.5 771 650 743.5zm807.9 106 348.8 200.8c162.5 93.7 217.6 300.6 123.8 462.8l.6.6c-39.8 68.6-102.4 121.2-176.5 148.2v-428c0-21.4-11-41-29.4-51.4l-422.3-243.7 155-89.3zM1201.7 997l177.8 102.8v205.1l-177.8 102.8-177.8-102.8v-205.1L1201.7 997zm279.5 161.6 155.1 89.4v402.2c0 187.3-152 339.2-339 339.2v-.6c-79.1 0-156.3-27.6-217-78.4 2.5-1.2 8-4.3 11-6.1l360.4-207.5c18.4-10.4 30-30 29.4-51.4l.1-486.8zM1380 1421.9v178.8l-348.8 200.8c-162.5 93.1-369.6 38-463.4-123.7h.6c-39.8-68-54-148.8-40.5-226.5 2.5 1.8 7.4 4.3 10.4 6.1l360.4 208.2c18.4 10.4 41 10.4 59.4 0l421.9-243.7z" fill="white"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2406 2406">
+    <path
+            d="M1 578.4C1 259.5 259.5 1 578.4 1h1249.1c319 0 577.5 258.5 577.5 577.4V2406H578.4C259.5 2406 1 2147.5 1 1828.6V578.4z"
+            fill="#74aa9c"/>
+    <path
+            d="M1107.3 299.1c-198 0-373.9 127.3-435.2 315.3C544.8 640.6 434.9 720.2 370.5 833c-99.3 171.4-76.6 386.9 56.4 533.8-41.1 123.1-27 257.7 38.6 369.2 98.7 172 297.3 260.2 491.6 219.2 86.1 97 209.8 152.3 339.6 151.8 198 0 373.9-127.3 435.3-315.3 127.5-26.3 237.2-105.9 301-218.5 99.9-171.4 77.2-386.9-55.8-533.9v-.6c41.1-123.1 27-257.8-38.6-369.8-98.7-171.4-297.3-259.6-491-218.6-86.6-96.8-210.5-151.8-340.3-151.2zm0 117.5-.6.6c79.7 0 156.3 27.5 217.6 78.4-2.5 1.2-7.4 4.3-11 6.1L952.8 709.3c-18.4 10.4-29.4 30-29.4 51.4V1248l-155.1-89.4V755.8c-.1-187.1 151.6-338.9 339-339.2zm434.2 141.9c121.6-.2 234 64.5 294.7 169.8 39.2 68.6 53.9 148.8 40.4 226.5-2.5-1.8-7.3-4.3-10.4-6.1l-360.4-208.2c-18.4-10.4-41-10.4-59.4 0L1024 984.2V805.4L1372.7 604c51.3-29.7 109.5-45.4 168.8-45.5zM650 743.5v427.9c0 21.4 11 40.4 29.4 51.4l421.7 243-155.7 90L597.2 1355c-162-93.8-217.4-300.9-123.8-462.8C513.1 823.6 575.5 771 650 743.5zm807.9 106 348.8 200.8c162.5 93.7 217.6 300.6 123.8 462.8l.6.6c-39.8 68.6-102.4 121.2-176.5 148.2v-428c0-21.4-11-41-29.4-51.4l-422.3-243.7 155-89.3zM1201.7 997l177.8 102.8v205.1l-177.8 102.8-177.8-102.8v-205.1L1201.7 997zm279.5 161.6 155.1 89.4v402.2c0 187.3-152 339.2-339 339.2v-.6c-79.1 0-156.3-27.6-217-78.4 2.5-1.2 8-4.3 11-6.1l360.4-207.5c18.4-10.4 30-30 29.4-51.4l.1-486.8zM1380 1421.9v178.8l-348.8 200.8c-162.5 93.1-369.6 38-463.4-123.7h.6c-39.8-68-54-148.8-40.5-226.5 2.5 1.8 7.4 4.3 10.4 6.1l360.4 208.2c18.4 10.4 41 10.4 59.4 0l421.9-243.7z"
+            fill="white"/>
+</svg>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
frontend/src/assets/svg/cpu.svg


+ 8 - 1
frontend/src/assets/svg/memory.svg

@@ -1 +1,8 @@
-<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683972914887" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5222" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M922.688 810.624h-149.312a37.376 37.376 0 0 1-37.312-37.376L736 736h-49.792v74.688H611.584V736h-49.792v74.688H462.208V736h-49.728v74.688H337.792V736H288l-0.256 37.248a37.312 37.312 0 0 1-37.312 37.376h-149.12A37.312 37.312 0 0 1 64 773.248V250.752c0-20.672 16.704-37.376 37.312-37.376h821.312a37.312 37.312 0 0 1 37.376 37.376v522.496a37.312 37.312 0 0 1-37.312 37.376z m-37.376-177.344h-39.168a37.376 37.376 0 0 1 0-74.752h39.168V288.128H138.688v270.4h37.952a37.376 37.376 0 0 1 0 74.752h-37.952v102.528H213.12l0.256-49.6c0-20.672 4.288-24.896 24.896-24.896h547.584c20.608 0 24.896 4.224 24.896 24.896v49.6h74.624V633.28zM736 337.792h74.688V512H736V337.792z m-174.208 0h74.688V512H561.792V337.792z m-174.208 0h74.688V512H387.584V337.792z m-174.272 0H288V512l-73.472-0.128-1.216-174.08z" p-id="5223"></path></svg>
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+        "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1683972914887" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5222"
+     width="200" height="200">
+    <path
+            d="M922.688 810.624h-149.312a37.376 37.376 0 0 1-37.312-37.376L736 736h-49.792v74.688H611.584V736h-49.792v74.688H462.208V736h-49.728v74.688H337.792V736H288l-0.256 37.248a37.312 37.312 0 0 1-37.312 37.376h-149.12A37.312 37.312 0 0 1 64 773.248V250.752c0-20.672 16.704-37.376 37.312-37.376h821.312a37.312 37.312 0 0 1 37.376 37.376v522.496a37.312 37.312 0 0 1-37.312 37.376z m-37.376-177.344h-39.168a37.376 37.376 0 0 1 0-74.752h39.168V288.128H138.688v270.4h37.952a37.376 37.376 0 0 1 0 74.752h-37.952v102.528H213.12l0.256-49.6c0-20.672 4.288-24.896 24.896-24.896h547.584c20.608 0 24.896 4.224 24.896 24.896v49.6h74.624V633.28zM736 337.792h74.688V512H736V337.792z m-174.208 0h74.688V512H561.792V337.792z m-174.208 0h74.688V512H387.584V337.792z m-174.272 0H288V512l-73.472-0.128-1.216-174.08z"
+            p-id="5223"></path>
+</svg>

+ 8 - 1
frontend/src/assets/svg/pulse.svg

@@ -1 +1,8 @@
-<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683971747666" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8583" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M448 938.666667a21.333333 21.333333 0 0 1-20.38-15.06L272.6 419.833333l-61.52 123.04A21.333333 21.333333 0 0 1 192 554.666667H21.333333a21.333333 21.333333 0 0 1 0-42.666667h157.48l79.44-158.873333a21.333333 21.333333 0 0 1 39.466667 3.266666L444.666667 834 597.84 144.706667a21.333333 21.333333 0 0 1 41.213333-1.646667l155.013334 503.773333 61.52-123.04A21.333333 21.333333 0 0 1 874.666667 512h128a21.333333 21.333333 0 0 1 0 42.666667h-114.813334l-79.44 158.873333a21.333333 21.333333 0 0 1-39.466666-3.266667L622 232.666667 468.826667 921.96a21.333333 21.333333 0 0 1-20 16.666667z" fill="#5C5C66" p-id="8584"></path></svg>
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+        "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1683971747666" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8583"
+     width="200" height="200">
+    <path
+            d="M448 938.666667a21.333333 21.333333 0 0 1-20.38-15.06L272.6 419.833333l-61.52 123.04A21.333333 21.333333 0 0 1 192 554.666667H21.333333a21.333333 21.333333 0 0 1 0-42.666667h157.48l79.44-158.873333a21.333333 21.333333 0 0 1 39.466667 3.266666L444.666667 834 597.84 144.706667a21.333333 21.333333 0 0 1 41.213333-1.646667l155.013334 503.773333 61.52-123.04A21.333333 21.333333 0 0 1 874.666667 512h128a21.333333 21.333333 0 0 1 0 42.666667h-114.813334l-79.44 158.873333a21.333333 21.333333 0 0 1-39.466666-3.266667L622 232.666667 468.826667 921.96a21.333333 21.333333 0 0 1-20 16.666667z"
+            fill="#5C5C66" p-id="8584"></path>
+</svg>

+ 21 - 21
frontend/src/components/Breadcrumb/Breadcrumb.vue

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

+ 86 - 86
frontend/src/components/Chart/AreaChart.vue

@@ -10,115 +10,115 @@ const settings = useSettingsStore()
 const {theme} = storeToRefs(settings)
 
 const fontColor = () => {
-    return theme.value === 'dark' ? '#b4b4b4' : undefined
+  return theme.value === 'dark' ? '#b4b4b4' : undefined
 }
 
 const chart = ref(null)
 
 let chartOptions = {
-    chart: {
-        type: 'area',
-        zoom: {
-            enabled: false
-        },
-        animations: {
-            enabled: false
-        },
-        toolbar: {
-            show: false
-        }
+  chart: {
+    type: 'area',
+    zoom: {
+      enabled: false
     },
-    colors: ['#ff6385', '#36a3eb'],
-    fill: {
-        // type: ['solid', 'gradient'],
-        gradient: {
-            shade: 'light'
-        }
-        //colors:  ['#ff6385', '#36a3eb'],
+    animations: {
+      enabled: false
     },
-    dataLabels: {
-        enabled: false
+    toolbar: {
+      show: false
+    }
+  },
+  colors: ['#ff6385', '#36a3eb'],
+  fill: {
+    // type: ['solid', 'gradient'],
+    gradient: {
+      shade: 'light'
+    }
+    //colors:  ['#ff6385', '#36a3eb'],
+  },
+  dataLabels: {
+    enabled: false
+  },
+  stroke: {
+    curve: 'smooth',
+    width: 0
+  },
+  xaxis: {
+    type: 'datetime',
+    labels: {
+      datetimeUTC: false,
+      style: {
+        colors: fontColor()
+      }
+    }
+  },
+  tooltip: {
+    enabled: false
+  },
+  yaxis: {
+    max: max,
+    tickAmount: 4,
+    min: 0,
+    labels: {
+      style: {
+        colors: fontColor()
+      },
+      formatter: y_formatter
+    }
+  },
+  legend: {
+    labels: {
+      colors: fontColor()
     },
-    stroke: {
-        curve: 'smooth',
-        width: 0
+    onItemClick: {
+      toggleDataSeries: false
     },
-    xaxis: {
+    onItemHover: {
+      highlightDataSeries: false
+    }
+  }
+}
+
+let instance: ApexCharts | null = chart.value
+
+const callback = () => {
+  chartOptions = {
+    ...chartOptions,
+    ...{
+      xaxis: {
         type: 'datetime',
         labels: {
-            datetimeUTC: false,
-            style: {
-                colors: fontColor()
-            }
+          datetimeUTC: false,
+          style: {
+            colors: fontColor()
+          }
         }
-    },
-    tooltip: {
-        enabled: false
-    },
-    yaxis: {
+      },
+      yaxis: {
         max: max,
         tickAmount: 4,
         min: 0,
         labels: {
-            style: {
-                colors: fontColor()
-            },
-            formatter: y_formatter
+          style: {
+            colors: fontColor()
+          },
+          formatter: y_formatter
         }
-    },
-    legend: {
+      },
+      legend: {
         labels: {
-            colors: fontColor()
+          colors: fontColor()
         },
         onItemClick: {
-            toggleDataSeries: false
+          toggleDataSeries: false
         },
         onItemHover: {
-            highlightDataSeries: false
-        }
-    }
-}
-
-let instance: ApexCharts | null = chart.value
-
-const callback = () => {
-    chartOptions = {
-        ...chartOptions,
-        ...{
-            xaxis: {
-                type: 'datetime',
-                labels: {
-                    datetimeUTC: false,
-                    style: {
-                        colors: fontColor()
-                    }
-                }
-            },
-            yaxis: {
-                max: max,
-                tickAmount: 4,
-                min: 0,
-                labels: {
-                    style: {
-                        colors: fontColor()
-                    },
-                    formatter: y_formatter
-                }
-            },
-            legend: {
-                labels: {
-                    colors: fontColor()
-                },
-                onItemClick: {
-                    toggleDataSeries: false
-                },
-                onItemHover: {
-                    highlightDataSeries: false
-                }
-            }
+          highlightDataSeries: false
         }
+      }
     }
-    instance?.updateOptions?.(chartOptions)
+  }
+  instance?.updateOptions?.(chartOptions)
 }
 
 
@@ -129,7 +129,7 @@ watch(theme, callback)
 </script>
 
 <template>
-    <VueApexCharts type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+  <VueApexCharts type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
 </template>
 
 

+ 77 - 70
frontend/src/components/Chart/RadialBarChart.vue

@@ -1,95 +1,102 @@
 <script setup lang="ts">
 import VueApexCharts from 'vue3-apexcharts'
 import {reactive} from 'vue'
+import {useSettingsStore} from '@/pinia'
+import {storeToRefs} from 'pinia'
 
 const {series, centerText, colors, name, bottomText}
-    = defineProps(['series', 'centerText', 'colors', 'name', 'bottomText'])
+  = defineProps(['series', 'centerText', 'colors', 'name', 'bottomText'])
+
+const settings = useSettingsStore()
+
+const {theme} = storeToRefs(settings)
 
 const chartOptions = reactive({
-    series: series,
-    chart: {
-        type: 'radialBar',
-        offsetY: 0
-    },
-    plotOptions: {
-        radialBar: {
-            startAngle: -135,
-            endAngle: 135,
-            dataLabels: {
-                name: {
-                    fontSize: '14px',
-                    color: colors,
-                    offsetY: 36
-                },
-                value: {
-                    offsetY: 50,
-                    fontSize: '14px',
-                    color: undefined,
-                    formatter: () => {
-                        return ''
-                    }
-                }
-            }
-        }
-    },
-    fill: {
-        colors: colors
-    },
-    labels: [name],
-    states: {
-        hover: {
-            filter: {
-                type: 'none'
-            }
+  series: series,
+  chart: {
+    type: 'radialBar',
+    offsetY: 0
+  },
+  plotOptions: {
+    radialBar: {
+      startAngle: -135,
+      endAngle: 135,
+      dataLabels: {
+        name: {
+          fontSize: '14px',
+          color: colors,
+          offsetY: 36
         },
-        active: {
-            filter: {
-                type: 'none'
-            }
+        value: {
+          offsetY: 50,
+          fontSize: '14px',
+          color: undefined,
+          formatter: () => {
+            return ''
+          }
         }
+      }
     }
+  },
+  fill: {
+    colors: colors
+  },
+  labels: [name],
+  states: {
+    hover: {
+      filter: {
+        type: 'none'
+      }
+    },
+    active: {
+      filter: {
+        type: 'none'
+      }
+    }
+  }
 })
 </script>
 
 <template>
-    <div class="radial-bar-container">
-        <p class="text">{{ centerText }}</p>
-        <p class="bottom_text">{{ bottomText }}</p>
-        <VueApexCharts v-if="centerText" class="radialBar" type="radialBar" height="205" :options="chartOptions"
-                       :series="series"
-                       ref="chart"/>
-    </div>
+  <!-- Use theme as key to rerender the chart when theme changes to prevent style issues -->
+  <div class="radial-bar-container" :key="theme">
+    <p class="text">{{ centerText }}</p>
+    <p class="bottom_text">{{ bottomText }}</p>
+    <VueApexCharts v-if="centerText" class="radialBar" type="radialBar" height="205" :options="chartOptions"
+                   :series="series"
+                   ref="chart"/>
+  </div>
 </template>
 
 
 <style lang="less" scoped>
 .radial-bar-container {
-    position: relative;
-    margin: 0 auto;
-    height: 112px !important;
+  position: relative;
+  margin: 0 auto;
+  height: 112px !important;
 
-    .radialBar {
-        position: absolute;
-        top: -30px;
-        @media (max-width: 768px) and (min-width: 290px) {
-            left: 50%;
-            transform: translateX(-50%);
-        }
+  .radialBar {
+    position: absolute;
+    top: -30px;
+    @media (max-width: 768px) and (min-width: 290px) {
+      left: 50%;
+      transform: translateX(-50%);
     }
+  }
 
-    .text {
-        position: absolute;
-        top: calc(50% - 5px);
-        width: 100%;
-        text-align: center;
-    }
+  .text {
+    position: absolute;
+    top: calc(50% - 5px);
+    width: 100%;
+    text-align: center;
+  }
 
-    .bottom_text {
-        position: absolute;
-        top: calc(106px);
-        font-weight: 600;
-        width: 100%;
-        text-align: center;
-    }
+  .bottom_text {
+    position: absolute;
+    top: calc(106px);
+    font-weight: 600;
+    width: 100%;
+    text-align: center;
+  }
 }
 </style>

+ 23 - 23
frontend/src/components/Chart/UsageProgressLine.vue

@@ -2,51 +2,51 @@
 import {computed} from 'vue'
 
 const props = withDefaults(defineProps<{
-    percent: number
+  percent: number
 }>(), {
-    percent: 0
+  percent: 0
 })
 
 const color = computed(() => {
-    if (props.percent < 80) {
-        return '#1890ff'
-    } else if (props.percent >= 80 && props.percent < 90) {
-        return '#faad14'
-    } else {
-        return '#ff6385'
-    }
+  if (props.percent < 80) {
+    return '#1890ff'
+  } else if (props.percent >= 80 && props.percent < 90) {
+    return '#faad14'
+  } else {
+    return '#ff6385'
+  }
 })
 
 const fixed_percent = computed(() => {
-    return parseFloat(props.percent.toFixed(2))
+  return parseFloat(props.percent.toFixed(2))
 })
 </script>
 
 <template>
+  <div>
     <div>
-        <div>
-            <span class="slot-icon"><slot name="icon"></slot></span>
-            <span class="slot">
+      <span class="slot-icon"><slot name="icon"></slot></span>
+      <span class="slot">
                 <slot></slot>
             </span>
-            <span class="dot"> ·</span> {{ fixed_percent + '%' }}
-        </div>
-        <a-progress :percent="fixed_percent" :stroke-color="color" :show-info="false"/>
+      <span class="dot"> ·</span> {{ fixed_percent + '%' }}
     </div>
+    <a-progress :percent="fixed_percent" :stroke-color="color" :show-info="false"/>
+  </div>
 </template>
 
 <style scoped lang="less">
 .slot-icon {
-    margin-right: 5px;
+  margin-right: 5px;
 }
 
 @media (max-width: 1000px) and  (min-width: 600px) {
-    .dot {
-        display: none;
-    }
+  .dot {
+    display: none;
+  }
 
-    .slot {
-        display: none;
-    }
+  .slot {
+    display: none;
+  }
 }
 </style>

+ 206 - 206
frontend/src/components/ChatGPT/ChatGPT.vue

@@ -19,11 +19,11 @@ const emit = defineEmits(['update:history_messages'])
 const history_messages = computed(() => props.history_messages)
 
 onMounted(() => {
-    messages.value = props.history_messages
+  messages.value = props.history_messages
 })
 
 watch(history_messages, () => {
-    messages.value = props.history_messages
+  messages.value = props.history_messages
 })
 
 const {current} = useGettext()
@@ -34,153 +34,153 @@ const loading = ref(false)
 const ask_buffer = ref('')
 
 async function request() {
-    loading.value = true
-    const t = ref({
-        role: 'assistant',
-        content: ''
-    })
-    const user = useUserStore()
+  loading.value = true
+  const t = ref({
+    role: 'assistant',
+    content: ''
+  })
+  const user = useUserStore()
 
-    const {token} = storeToRefs(user)
+  const {token} = storeToRefs(user)
 
-    console.log('fetching...')
+  console.log('fetching...')
 
-    messages.value.push(t.value)
+  messages.value.push(t.value)
 
-    emit('update:history_messages', messages.value)
+  emit('update:history_messages', messages.value)
 
-    let res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
-        method: 'POST',
-        headers: {'Accept': 'text/event-stream', Authorization: token.value},
-        body: JSON.stringify({messages: messages.value.slice(0, messages.value?.length - 1)})
-    })
-    // read body as stream
-    console.log('reading...')
-    let reader = res.body!.getReader()
+  let res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
+    method: 'POST',
+    headers: {'Accept': 'text/event-stream', Authorization: token.value},
+    body: JSON.stringify({messages: messages.value.slice(0, messages.value?.length - 1)})
+  })
+  // read body as stream
+  console.log('reading...')
+  let reader = res.body!.getReader()
 
-    // read stream
-    console.log('reading stream...')
+  // read stream
+  console.log('reading stream...')
 
-    let buffer = ''
+  let buffer = ''
 
-    let hasCodeBlockIndicator = false
+  let hasCodeBlockIndicator = false
 
-    while (true) {
-        let {done, value} = await reader.read()
-        if (done) {
-            console.log('done')
-            loading.value = false
-            store_record()
-            break
-        }
-
-        apply(value)
+  while (true) {
+    let {done, value} = await reader.read()
+    if (done) {
+      console.log('done')
+      loading.value = false
+      store_record()
+      break
     }
 
-    function apply(input: any) {
-        const decoder = new TextDecoder('utf-8')
-        const raw = decoder.decode(input)
-
-        // console.log(input, raw)
-
-        const line = raw.split('\n\n')
-
-        line?.forEach(v => {
-            const data = v.slice('event:message\ndata:'.length)
-            if (!data) {
-                return
-            }
-            const content = JSON.parse(data).content
-
-            if (!hasCodeBlockIndicator) {
-                hasCodeBlockIndicator = content.indexOf('`') > -1
-            }
-
-            for (let c of content) {
-                buffer += c
-                if (hasCodeBlockIndicator) {
-                    if (isCodeBlockComplete(buffer)) {
-                        t.value.content = buffer
-                        hasCodeBlockIndicator = false
-                    } else {
-                        t.value.content = buffer + '\n```'
-                    }
-                } else {
-                    t.value.content = buffer
-                }
-            }
-        })
-    }
-
-    function isCodeBlockComplete(text: string) {
-        const codeBlockRegex = /```/g
-        const matches = text.match(codeBlockRegex)
-        if (matches) {
-            return matches.length % 2 === 0
+    apply(value)
+  }
+
+  function apply(input: any) {
+    const decoder = new TextDecoder('utf-8')
+    const raw = decoder.decode(input)
+
+    // console.log(input, raw)
+
+    const line = raw.split('\n\n')
+
+    line?.forEach(v => {
+      const data = v.slice('event:message\ndata:'.length)
+      if (!data) {
+        return
+      }
+      const content = JSON.parse(data).content
+
+      if (!hasCodeBlockIndicator) {
+        hasCodeBlockIndicator = content.indexOf('`') > -1
+      }
+
+      for (let c of content) {
+        buffer += c
+        if (hasCodeBlockIndicator) {
+          if (isCodeBlockComplete(buffer)) {
+            t.value.content = buffer
+            hasCodeBlockIndicator = false
+          } else {
+            t.value.content = buffer + '\n```'
+          }
         } else {
-            return true
+          t.value.content = buffer
         }
+      }
+    })
+  }
+
+  function isCodeBlockComplete(text: string) {
+    const codeBlockRegex = /```/g
+    const matches = text.match(codeBlockRegex)
+    if (matches) {
+      return matches.length % 2 === 0
+    } else {
+      return true
     }
+  }
 
 }
 
 async function send() {
-    if (!messages.value) {
-        messages.value = []
-    }
-    if (messages.value.length === 0) {
-        messages.value.push({
-            role: 'user',
-            content: props.content + '\n\nCurrent Language Code: ' + current
-        })
-    } else {
-        messages.value.push({
-            role: 'user',
-            content: ask_buffer.value
-        })
-        ask_buffer.value = ''
-    }
-    await request()
+  if (!messages.value) {
+    messages.value = []
+  }
+  if (messages.value.length === 0) {
+    messages.value.push({
+      role: 'user',
+      content: props.content + '\n\nCurrent Language Code: ' + current
+    })
+  } else {
+    messages.value.push({
+      role: 'user',
+      content: ask_buffer.value
+    })
+    ask_buffer.value = ''
+  }
+  await request()
 }
 
 const renderer = new marked.Renderer()
 renderer.code = (code, lang: string) => {
-    const language = hljs.getLanguage(lang) ? lang : 'nginx'
-    const highlightedCode = hljs.highlight(code, {language}).value
-    return `<pre><code class="hljs ${language}">${highlightedCode}</code></pre>`
+  const language = hljs.getLanguage(lang) ? lang : 'nginx'
+  const highlightedCode = hljs.highlight(code, {language}).value
+  return `<pre><code class="hljs ${language}">${highlightedCode}</code></pre>`
 }
 
 marked.setOptions({
-    renderer: renderer,
-    langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
-    pedantic: false,
-    gfm: true,
-    breaks: false,
-    sanitize: false,
-    smartypants: true,
-    xhtml: false
+  renderer: renderer,
+  langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
+  pedantic: false,
+  gfm: true,
+  breaks: false,
+  sanitize: false,
+  smartypants: true,
+  xhtml: false
 })
 
 function store_record() {
-    openai.store_record({
-        file_name: props.path,
-        messages: messages.value
-    })
+  openai.store_record({
+    file_name: props.path,
+    messages: messages.value
+  })
 }
 
 function clear_record() {
-    openai.store_record({
-        file_name: props.path,
-        messages: []
-    })
-    messages.value = []
-    emit('update:history_messages', [])
+  openai.store_record({
+    file_name: props.path,
+    messages: []
+  })
+  messages.value = []
+  emit('update:history_messages', [])
 }
 
 async function regenerate(index: number) {
-    editing_idx.value = -1
-    messages.value = messages.value.slice(0, index)
-    await request()
+  editing_idx.value = -1
+  messages.value = messages.value.slice(0, index)
+  await request()
 }
 
 const editing_idx = ref(-1)
@@ -190,120 +190,120 @@ const show = computed(() => messages?.value?.length === 0)
 </script>
 
 <template>
-    <div class="chat-start" v-if="show">
-        <a-button @click="send" :loading="loading">
-            <Icon v-if="!loading" :component="ChatGPT_logo"/>
-            {{ $gettext('Ask ChatGPT for Help') }}
-        </a-button>
-    </div>
-    <div class="chatgpt-container" v-else>
-        <a-list
-            class="chatgpt-log"
-            item-layout="horizontal"
-            :data-source="messages"
-        >
-            <template #renderItem="{ item, index }">
-                <a-list-item>
-                    <a-comment :author="item.role==='assistant'?$gettext('Assistant'):$gettext('User')">
-                        <template #content>
-                            <div class="content" v-if="item.role==='assistant'||editing_idx!=index"
-                                 v-html="marked.parse(item.content)"></div>
-                            <a-input style="padding: 0" v-else v-model:value="item.content"
-                                     :bordered="false"/>
-                        </template>
-                        <template #actions>
+  <div class="chat-start" v-if="show">
+    <a-button @click="send" :loading="loading">
+      <Icon v-if="!loading" :component="ChatGPT_logo"/>
+      {{ $gettext('Ask ChatGPT for Help') }}
+    </a-button>
+  </div>
+  <div class="chatgpt-container" v-else>
+    <a-list
+      class="chatgpt-log"
+      item-layout="horizontal"
+      :data-source="messages"
+    >
+      <template #renderItem="{ item, index }">
+        <a-list-item>
+          <a-comment :author="item.role==='assistant'?$gettext('Assistant'):$gettext('User')">
+            <template #content>
+              <div class="content" v-if="item.role==='assistant'||editing_idx!=index"
+                   v-html="marked.parse(item.content)"></div>
+              <a-input style="padding: 0" v-else v-model:value="item.content"
+                       :bordered="false"/>
+            </template>
+            <template #actions>
                                     <span v-if="item.role==='user'&&editing_idx!==index" @click="editing_idx=index">
                                         {{ $gettext('Modify') }}
                                     </span>
-                            <template v-else-if="editing_idx==index">
-                                <span @click="regenerate(index+1)">{{ $gettext('Save') }}</span>
-                                <span @click="editing_idx=-1">{{ $gettext('Cancel') }}</span>
-                            </template>
-                            <span v-else-if="!loading" @click="regenerate(index)" :disabled="loading">
+              <template v-else-if="editing_idx==index">
+                <span @click="regenerate(index+1)">{{ $gettext('Save') }}</span>
+                <span @click="editing_idx=-1">{{ $gettext('Cancel') }}</span>
+              </template>
+              <span v-else-if="!loading" @click="regenerate(index)" :disabled="loading">
                                         {{ $gettext('Reload') }}
                                     </span>
-                        </template>
-                    </a-comment>
-                </a-list-item>
             </template>
-        </a-list>
-        <div class="input-msg">
-            <div class="control-btn">
-                <a-space v-show="!loading">
-                    <a-popconfirm
-                        :cancelText="$gettext('No')"
-                        :okText="$gettext('OK')"
-                        :title="$gettext('Are you sure you want to clear the record of chat?')"
-                        @confirm="clear_record">
-                        <a-button type="text">{{ $gettext('Clear') }}</a-button>
-                    </a-popconfirm>
-                    <a-button type="text" @click="regenerate(messages?.length-1)">
-                        {{ $gettext('Regenerate response') }}
-                    </a-button>
-                </a-space>
-            </div>
-            <a-textarea auto-size v-model:value="ask_buffer"/>
-            <div class="sned-btn">
-                <a-button size="small" type="text" :loading="loading" @click="send">
-                    <send-outlined/>
-                </a-button>
-            </div>
-        </div>
+          </a-comment>
+        </a-list-item>
+      </template>
+    </a-list>
+    <div class="input-msg">
+      <div class="control-btn">
+        <a-space v-show="!loading">
+          <a-popconfirm
+            :cancelText="$gettext('No')"
+            :okText="$gettext('OK')"
+            :title="$gettext('Are you sure you want to clear the record of chat?')"
+            @confirm="clear_record">
+            <a-button type="text">{{ $gettext('Clear') }}</a-button>
+          </a-popconfirm>
+          <a-button type="text" @click="regenerate(messages?.length-1)">
+            {{ $gettext('Regenerate response') }}
+          </a-button>
+        </a-space>
+      </div>
+      <a-textarea auto-size v-model:value="ask_buffer"/>
+      <div class="sned-btn">
+        <a-button size="small" type="text" :loading="loading" @click="send">
+          <send-outlined/>
+        </a-button>
+      </div>
     </div>
+  </div>
 </template>
 
 <style lang="less" scoped>
 .chatgpt-container {
-    margin: 0 auto;
-    max-width: 800px;
+  margin: 0 auto;
+  max-width: 800px;
 
-    .chatgpt-log {
-        .content {
-            width: 100%;
+  .chatgpt-log {
+    .content {
+      width: 100%;
 
-            :deep(.hljs) {
-                border-radius: 5px;
-            }
-        }
+      :deep(.hljs) {
+        border-radius: 5px;
+      }
+    }
 
-        :deep(.ant-list-item) {
-            padding: 0;
-        }
+    :deep(.ant-list-item) {
+      padding: 0;
+    }
 
-        :deep(.ant-comment-content) {
-            width: 100%;
-        }
+    :deep(.ant-comment-content) {
+      width: 100%;
+    }
 
-        :deep(.ant-comment) {
-            width: 100%;
-        }
+    :deep(.ant-comment) {
+      width: 100%;
+    }
 
-        :deep(.ant-comment-content-detail) {
-            width: 100%;
+    :deep(.ant-comment-content-detail) {
+      width: 100%;
 
-            p {
-                margin-bottom: 10px;
-            }
-        }
+      p {
+        margin-bottom: 10px;
+      }
+    }
 
-        :deep(.ant-list-item:first-child) {
-            display: none;
-        }
+    :deep(.ant-list-item:first-child) {
+      display: none;
     }
+  }
 
-    .input-msg {
-        position: relative;
+  .input-msg {
+    position: relative;
 
-        .control-btn {
-            display: flex;
-            justify-content: center;
-        }
+    .control-btn {
+      display: flex;
+      justify-content: center;
+    }
 
-        .sned-btn {
-            position: absolute;
-            right: 0;
-            bottom: 3px;
-        }
+    .sned-btn {
+      position: absolute;
+      right: 0;
+      bottom: 3px;
     }
+  }
 }
 </style>

+ 11 - 11
frontend/src/components/CodeEditor/CodeEditor.vue

@@ -9,21 +9,21 @@ const props = defineProps(['content', 'defaultHeight'])
 const emit = defineEmits(['update:content'])
 
 const value = computed({
-    get() {
-        return props.content ?? ''
-    },
-    set(value) {
-        emit('update:content', value)
-    }
+  get() {
+    return props.content ?? ''
+  },
+  set(value) {
+    emit('update:content', value)
+  }
 })
 </script>
 
 <template>
-    <v-ace-editor
-        v-model:value="value"
-        lang="nginx"
-        theme="monokai"
-        :style="{
+  <v-ace-editor
+    v-model:value="value"
+    lang="nginx"
+    theme="monokai"
+    :style="{
             minHeight: defaultHeight || '100vh',
             borderRadius: '5px'
         }"/>

+ 49 - 49
frontend/src/components/EnvIndicator/EnvIndicator.vue

@@ -13,88 +13,88 @@ const {environment} = storeToRefs(settingsStore)
 const router = useRouter()
 
 async function clear_env() {
-    await router.push('/dashboard')
-    settingsStore.clear_environment()
+  await router.push('/dashboard')
+  settingsStore.clear_environment()
 }
 
 const is_local = computed(() => {
-    return environment.value.id === 0
+  return environment.value.id === 0
 })
 
 const node_id = computed(() => environment.value.id)
 
 watch(node_id, async () => {
-    await router.push('/dashboard')
-    location.reload()
+  await router.push('/dashboard')
+  location.reload()
 })
 </script>
 
 <template>
-    <div class="indicator">
-        <div class="container">
-            <database-outlined/>
-            <span class="env-name" v-if="is_local">
+  <div class="indicator">
+    <div class="container">
+      <database-outlined/>
+      <span class="env-name" v-if="is_local">
                  {{ $gettext('Local') }}
             </span>
-            <span class="env-name" v-else>
+      <span class="env-name" v-else>
                  {{ environment.name }}
             </span>
-            <a-tag @click="clear_env">
-                <dashboard-outlined v-if="is_local"/>
-                <close-outlined v-else/>
-            </a-tag>
-        </div>
+      <a-tag @click="clear_env">
+        <dashboard-outlined v-if="is_local"/>
+        <close-outlined v-else/>
+      </a-tag>
     </div>
+  </div>
 </template>
 
 <style scoped lang="less">
 .ant-layout-sider-collapsed {
-    .ant-tag, .env-name {
-        display: none;
-    }
+  .ant-tag, .env-name {
+    display: none;
+  }
 
-    .indicator {
-        .container {
-            justify-content: center;
-        }
+  .indicator {
+    .container {
+      justify-content: center;
     }
+  }
 }
 
 .indicator {
-    padding: 20px 20px 16px 20px;
+  padding: 20px 20px 16px 20px;
 
-    .container {
-        border-radius: 16px;
-        border: 1px solid #91d5ff;
-        background: #e6f7ff;
-        padding: 5px 15px;
-        color: #096dd9;
+  .container {
+    border-radius: 16px;
+    border: 1px solid #91d5ff;
+    background: #e6f7ff;
+    padding: 5px 15px;
+    color: #096dd9;
 
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
 
-        .env-name {
-            max-width: 85px;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-            line-height: 1em;
-            overflow: hidden;
-        }
+    .env-name {
+      max-width: 85px;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      line-height: 1em;
+      overflow: hidden;
+    }
 
-        .ant-tag {
-            cursor: pointer;
-            margin-right: 0;
-            padding: 0 5px;
-        }
+    .ant-tag {
+      cursor: pointer;
+      margin-right: 0;
+      padding: 0 5px;
     }
+  }
 }
 
 .dark {
-    .container {
-        border: 1px solid #545454;
-        background: transparent;
-        color: #bebebe;
-    }
+  .container {
+    border: 1px solid #545454;
+    background: transparent;
+    color: #bebebe;
+  }
 }
 </style>

+ 37 - 37
frontend/src/components/FooterToolbar/FooterToolBar.vue

@@ -1,55 +1,55 @@
 <template>
-    <div class="ant-pro-footer-toolbar">
-        <div style="float: left">
-            <slot name="extra">{{ extra }}</slot>
-        </div>
-        <div style="float: right">
-            <slot></slot>
-        </div>
+  <div class="ant-pro-footer-toolbar">
+    <div style="float: left">
+      <slot name="extra">{{ extra }}</slot>
     </div>
+    <div style="float: right">
+      <slot></slot>
+    </div>
+  </div>
 </template>
 
 <script>
 export default {
-    name: 'FooterToolBar',
-    props: {
-        prefixCls: {
-            type: String,
-            default: 'ant-pro-footer-toolbar'
-        },
-        extra: {
-            type: [String, Object],
-            default: ''
-        }
+  name: 'FooterToolBar',
+  props: {
+    prefixCls: {
+      type: String,
+      default: 'ant-pro-footer-toolbar'
+    },
+    extra: {
+      type: [String, Object],
+      default: ''
     }
+  }
 }
 </script>
 
 <style lang="less" scoped>
 .dark {
-    .ant-pro-footer-toolbar {
-        background: rgba(24, 24, 24, 1);
-        border-top: unset;
-    }
+  .ant-pro-footer-toolbar {
+    background: rgba(24, 24, 24, 1);
+    border-top: unset;
+  }
 }
 
 .ant-pro-footer-toolbar {
-    position: fixed;
-    width: 100%;
-    bottom: 0;
-    right: 0;
-    height: 56px;
-    line-height: 56px;
-    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
-    background: #ffffff8c;
-    border-top: 1px solid #e8e8e8;
-    padding: 0 24px;
-    z-index: 9;
+  position: fixed;
+  width: 100%;
+  bottom: 0;
+  right: 0;
+  height: 56px;
+  line-height: 56px;
+  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
+  background: #ffffff8c;
+  border-top: 1px solid #e8e8e8;
+  padding: 0 24px;
+  z-index: 9;
 
-    &:after {
-        content: "";
-        display: block;
-        clear: both;
-    }
+  &:after {
+    content: "";
+    display: block;
+    clear: both;
+  }
 }
 </style>

+ 25 - 25
frontend/src/components/Logo/Logo.vue

@@ -2,39 +2,39 @@
 import logo from '@/assets/img/logo.png'</script>
 
 <template>
-    <div class="logo">
-        <img :src="logo" alt="logo"/>
-        <p class="text">Nginx UI</p>
-    </div>
+  <div class="logo">
+    <img :src="logo" alt="logo"/>
+    <p class="text">Nginx UI</p>
+  </div>
 </template>
 
 <style lang="less" scoped>
 .dark {
-    .logo {
-        background-color: transparent;
-        -webkit-box-shadow: 1px 1px 0 0 #404040;
-        box-shadow: 1px 1px 0 0 #404040;
-    }
+  .logo {
+    background-color: transparent;
+    -webkit-box-shadow: 1px 1px 0 0 #404040;
+    box-shadow: 1px 1px 0 0 #404040;
+  }
 }
 
 .logo {
-    -webkit-box-shadow: 1px 1px 0 0 #e8e8e8;
-    box-shadow: 1px 1px 0 0 #e8e8e8;
-    transition: all 0.3s;
-    height: 64px;
-    width: 100%;
-    overflow: hidden;
-    background-color: #ffffff;
+  -webkit-box-shadow: 1px 1px 0 0 #e8e8e8;
+  box-shadow: 1px 1px 0 0 #e8e8e8;
+  transition: all 0.3s;
+  height: 64px;
+  width: 100%;
+  overflow: hidden;
+  background-color: #ffffff;
 
-    img {
-        height: 46px;
-    }
+  img {
+    height: 46px;
+  }
 
-    p.text {
-        margin: 0;
-        font-size: 22px;
-        line-height: 48px;
-        height: 48px;
-    }
+  p.text {
+    margin: 0;
+    font-size: 22px;
+    line-height: 48px;
+    height: 48px;
+  }
 }
 </style>

+ 68 - 68
frontend/src/components/NginxControl/NginxControl.vue

@@ -10,47 +10,47 @@ import {ref, watch} from 'vue'
 const {$gettext} = gettext
 
 function get_status() {
-    ngx.status().then(r => {
-        if (r?.running === true) {
-            status.value = 0
-        } else {
-            status.value = -1
-        }
-    })
+  ngx.status().then(r => {
+    if (r?.running === true) {
+      status.value = 0
+    } else {
+      status.value = -1
+    }
+  })
 }
 
 function reload_nginx() {
-    status.value = 1
-    ngx.reload().then(r => {
-        if (r.level < logLevel.Warn) {
-            message.success($gettext('Nginx reloaded successfully'))
-        } else if (r.level === logLevel.Warn) {
-            message.warn(r.message)
-        } else {
-            message.error(r.message)
-        }
-    }).catch(e => {
-        message.error($gettext('Server error') + ' ' + e?.message)
-    }).finally(() => {
-        status.value = 0
-    })
+  status.value = 1
+  ngx.reload().then(r => {
+    if (r.level < logLevel.Warn) {
+      message.success($gettext('Nginx reloaded successfully'))
+    } else if (r.level === logLevel.Warn) {
+      message.warn(r.message)
+    } else {
+      message.error(r.message)
+    }
+  }).catch(e => {
+    message.error($gettext('Server error') + ' ' + e?.message)
+  }).finally(() => {
+    status.value = 0
+  })
 }
 
 function restart_nginx() {
-    status.value = 2
-    ngx.restart().then(r => {
-        if (r.level < logLevel.Warn) {
-            message.success($gettext('Nginx restarted successfully'))
-        } else if (r.level === logLevel.Warn) {
-            message.warn(r.message)
-        } else {
-            message.error(r.message)
-        }
-    }).catch(e => {
-        message.error($gettext('Server error') + ' ' + e?.message)
-    }).finally(() => {
-        status.value = 0
-    })
+  status.value = 2
+  ngx.restart().then(r => {
+    if (r.level < logLevel.Warn) {
+      message.success($gettext('Nginx restarted successfully'))
+    } else if (r.level === logLevel.Warn) {
+      message.warn(r.message)
+    } else {
+      message.error(r.message)
+    }
+  }).catch(e => {
+    message.error($gettext('Server error') + ' ' + e?.message)
+  }).finally(() => {
+    status.value = 0
+  })
 }
 
 const status = ref(0)
@@ -58,53 +58,53 @@ const status = ref(0)
 const visible = ref(false)
 
 watch(visible, (v) => {
-    if (v) get_status()
+  if (v) get_status()
 })
 </script>
 
 <template>
-    <a-popover
-        v-model:visible="visible"
-        @confirm="reload_nginx"
-        placement="bottomRight"
-    >
-        <template #content>
-            <div class="content-wrapper">
-                <h4>{{ $gettext('Nginx Control') }}</h4>
-                <a-badge v-if="status===0" color="green" :text="$gettext('Running')"/>
-                <a-badge v-else-if="status===1" color="blue" :text="$gettext('Reloading')"/>
-                <a-badge v-else-if="status===2" color="orange" :text="$gettext('Restarting')"/>
-                <a-badge v-else color="red" :text="$gettext('Stopped')"/>
-            </div>
-            <a-space>
-                <a-button size="small" @click="restart_nginx" type="link">{{ $gettext('Restart') }}</a-button>
-                <a-button size="small" @click="reload_nginx" type="link">{{ $gettext('Reload') }}</a-button>
-            </a-space>
-        </template>
-        <a>
-            <ReloadOutlined/>
-        </a>
-    </a-popover>
+  <a-popover
+    v-model:open="visible"
+    @confirm="reload_nginx"
+    placement="bottomRight"
+  >
+    <template #content>
+      <div class="content-wrapper">
+        <h4>{{ $gettext('Nginx Control') }}</h4>
+        <a-badge v-if="status===0" color="green" :text="$gettext('Running')"/>
+        <a-badge v-else-if="status===1" color="blue" :text="$gettext('Reloading')"/>
+        <a-badge v-else-if="status===2" color="orange" :text="$gettext('Restarting')"/>
+        <a-badge v-else color="red" :text="$gettext('Stopped')"/>
+      </div>
+      <a-space>
+        <a-button size="small" @click="restart_nginx" type="link">{{ $gettext('Restart') }}</a-button>
+        <a-button size="small" @click="reload_nginx" type="link">{{ $gettext('Reload') }}</a-button>
+      </a-space>
+    </template>
+    <a>
+      <ReloadOutlined/>
+    </a>
+  </a-popover>
 </template>
 
 <style lang="less" scoped>
 a {
-    color: #000000;
+  color: #000000;
 }
 
 .dark {
-    a {
-        color: #fafafa;
-    }
+  a {
+    color: #fafafa;
+  }
 }
 
 .content-wrapper {
-    text-align: center;
-    padding-top: 5px;
-    padding-bottom: 5px;
+  text-align: center;
+  padding-top: 5px;
+  padding-bottom: 5px;
 
-    h4 {
-        margin-bottom: 5px;
-    }
+  h4 {
+    margin-bottom: 5px;
+  }
 }
 </style>

+ 28 - 28
frontend/src/components/NodeSelector/NodeSelector.vue

@@ -12,42 +12,42 @@ const data = ref([])
 const data_map = ref({})
 
 environment.get_list().then(r => {
-    data.value = r.data
-    r.data.forEach(node => {
-        data_map[node.id] = node
-    })
+  data.value = r.data
+  r.data.forEach(node => {
+    data_map[node.id] = node
+  })
 })
 
 const value = computed({
-    get() {
-        return props.target
-    },
-    set(v) {
-        if (typeof props.map === 'object') {
-            v.forEach(id => {
-                if (id !== 0) props.map[id] = data_map[id].name
-            })
-        }
-        emit('update:target', v)
+  get() {
+    return props.target
+  },
+  set(v) {
+    if (typeof props.map === 'object') {
+      v.forEach(id => {
+        if (id !== 0) props.map[id] = data_map[id].name
+      })
     }
+    emit('update:target', v)
+  }
 })
 </script>
 
 <template>
-    <a-checkbox-group v-model:value="value" style="width: 100%">
-        <a-row :gutter="[16,16]">
-            <a-col :span="8" v-if="!hidden_local">
-                <a-checkbox :value="0">{{ $gettext('Local') }}</a-checkbox>
-                <a-tag color="blue">{{ $gettext('Online') }}</a-tag>
-            </a-col>
-            <a-col :span="8" v-for="node in data">
-                <a-checkbox :value="node.id">{{ node.name }}</a-checkbox>
-                <a-tag color="blue" v-if="node.status">{{ $gettext('Online') }}</a-tag>
-                <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
-            </a-col>
-        </a-row>
-        <a-empty v-if="hidden_local&&data.length===0"/>
-    </a-checkbox-group>
+  <a-checkbox-group v-model:value="value" style="width: 100%">
+    <a-row :gutter="[16,16]">
+      <a-col :span="8" v-if="!hidden_local">
+        <a-checkbox :value="0">{{ $gettext('Local') }}</a-checkbox>
+        <a-tag color="blue">{{ $gettext('Online') }}</a-tag>
+      </a-col>
+      <a-col :span="8" v-for="node in data">
+        <a-checkbox :value="node.id">{{ node.name }}</a-checkbox>
+        <a-tag color="blue" v-if="node.status">{{ $gettext('Online') }}</a-tag>
+        <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
+      </a-col>
+    </a-row>
+    <a-empty v-if="hidden_local&&data.length===0"/>
+  </a-checkbox-group>
 </template>
 
 <style scoped lang="less">

+ 143 - 143
frontend/src/components/PageHeader/PageHeader.vue

@@ -8,183 +8,183 @@ const {title, logo, avatar} = defineProps(['title', 'logo', 'avatar'])
 const route = useRoute()
 
 const display = computed(() => {
-    return !route.meta.hiddenHeaderContent
+  return !route.meta.hiddenHeaderContent
 })
 
 const name = ref(route.name)
 watch(() => route.name, () => {
-    name.value = route.name
+  name.value = route.name
 })
 
 </script>
 
 <template>
-    <div v-if="display" class="page-header">
-        <div class="page-header-index-wide">
-            <Breadcrumb/>
-            <div class="detail">
-                <div class="main">
-                    <div class="row">
-                        <img v-if="logo" :src="logo" class="logo"/>
-                        <h1 class="title">
-                            {{ name() }}
-                        </h1>
-                        <div class="action">
-                            <slot name="action"></slot>
-                        </div>
-                    </div>
-                </div>
+  <div v-if="display" class="page-header">
+    <div class="page-header-index-wide">
+      <Breadcrumb/>
+      <div class="detail">
+        <div class="main">
+          <div class="row">
+            <img v-if="logo" :src="logo" class="logo"/>
+            <h1 class="title">
+              {{ name() }}
+            </h1>
+            <div class="action">
+              <slot name="action"></slot>
             </div>
+          </div>
         </div>
+      </div>
     </div>
+  </div>
 </template>
 
 <style lang="less" scoped>
 .dark {
-    .page-header {
-        background: #28292c !important;
-        border-bottom: unset;
+  .page-header {
+    background: #28292c !important;
+    border-bottom: unset;
 
-        h1 {
-            color: #fafafa;
-        }
+    h1 {
+      color: #fafafa;
     }
+  }
 }
 
 .page-header {
-    background: #fff;
-    padding: 16px 32px 0;
-    border-bottom: 1px solid #e8e8e8;
-
-    .breadcrumb {
-        margin-bottom: 16px;
+  background: #fff;
+  padding: 16px 32px 0;
+  border-bottom: 1px solid #e8e8e8;
+
+  .breadcrumb {
+    margin-bottom: 16px;
+  }
+
+  .detail {
+    display: flex;
+    /*margin-bottom: 16px;*/
+
+    .avatar {
+      flex: 0 1 72px;
+      margin: 0 24px 8px 0;
+
+      & > span {
+        border-radius: 72px;
+        display: block;
+        width: 72px;
+        height: 72px;
+      }
     }
 
-    .detail {
+    .main {
+      width: 100%;
+      flex: 0 1 auto;
+
+      .row {
         display: flex;
-        /*margin-bottom: 16px;*/
+        width: 100%;
 
         .avatar {
-            flex: 0 1 72px;
-            margin: 0 24px 8px 0;
-
-            & > span {
-                border-radius: 72px;
-                display: block;
-                width: 72px;
-                height: 72px;
-            }
+          margin-bottom: 16px;
         }
+      }
 
-        .main {
-            width: 100%;
-            flex: 0 1 auto;
-
-            .row {
-                display: flex;
-                width: 100%;
-
-                .avatar {
-                    margin-bottom: 16px;
-                }
-            }
-
-            .title {
-                font-size: 20px;
-                font-weight: 500;
-                line-height: 28px;
-                margin-bottom: 16px;
-                flex: auto;
-            }
-
-            .logo {
-                width: 28px;
-                height: 28px;
-                border-radius: 4px;
-                margin-right: 16px;
-            }
-
-            .content,
-            .headerContent {
-                flex: auto;
-                line-height: 22px;
-
-                .link {
-                    margin-top: 16px;
-                    line-height: 24px;
-
-                    a {
-                        font-size: 14px;
-                        margin-right: 32px;
-                    }
-                }
-            }
-
-            .extra {
-                flex: 0 1 auto;
-                margin-left: 88px;
-                min-width: 242px;
-                text-align: right;
-            }
-
-            .action {
-                margin-left: 56px;
-                min-width: 266px;
-                flex: 0 1 auto;
-                text-align: right;
-
-                &:empty {
-                    display: none;
-                }
-            }
+      .title {
+        font-size: 20px;
+        font-weight: 500;
+        line-height: 28px;
+        margin-bottom: 16px;
+        flex: auto;
+      }
+
+      .logo {
+        width: 28px;
+        height: 28px;
+        border-radius: 4px;
+        margin-right: 16px;
+      }
+
+      .content,
+      .headerContent {
+        flex: auto;
+        line-height: 22px;
+
+        .link {
+          margin-top: 16px;
+          line-height: 24px;
+
+          a {
+            font-size: 14px;
+            margin-right: 32px;
+          }
+        }
+      }
+
+      .extra {
+        flex: 0 1 auto;
+        margin-left: 88px;
+        min-width: 242px;
+        text-align: right;
+      }
+
+      .action {
+        margin-left: 56px;
+        min-width: 266px;
+        flex: 0 1 auto;
+        text-align: right;
+
+        &:empty {
+          display: none;
         }
+      }
     }
+  }
 }
 
 .mobile .page-header {
-    .main {
-        .row {
-            flex-wrap: wrap;
-
-            .avatar {
-                flex: 0 1 25%;
-                margin: 0 2% 8px 0;
-            }
-
-            .content,
-            .headerContent {
-                flex: 0 1 70%;
-
-                .link {
-                    margin-top: 16px;
-                    line-height: 24px;
-
-                    a {
-                        font-size: 14px;
-                        margin-right: 10px;
-                    }
-                }
-            }
-
-            .extra {
-                flex: 1 1 auto;
-                margin-left: 0;
-                min-width: 0;
-                text-align: right;
-            }
-
-            .action {
-                margin-left: unset;
-                min-width: 266px;
-                flex: 0 1 auto;
-                text-align: left;
-                margin-bottom: 12px;
-
-                &:empty {
-                    display: none;
-                }
-            }
+  .main {
+    .row {
+      flex-wrap: wrap;
+
+      .avatar {
+        flex: 0 1 25%;
+        margin: 0 2% 8px 0;
+      }
+
+      .content,
+      .headerContent {
+        flex: 0 1 70%;
+
+        .link {
+          margin-top: 16px;
+          line-height: 24px;
+
+          a {
+            font-size: 14px;
+            margin-right: 10px;
+          }
+        }
+      }
+
+      .extra {
+        flex: 1 1 auto;
+        margin-left: 0;
+        min-width: 0;
+        text-align: right;
+      }
+
+      .action {
+        margin-left: unset;
+        min-width: 266px;
+        flex: 0 1 auto;
+        text-align: left;
+        margin-bottom: 12px;
+
+        &:empty {
+          display: none;
         }
+      }
     }
+  }
 }
 </style>

+ 17 - 17
frontend/src/components/SetLanguage/SetLanguage.vue

@@ -16,33 +16,33 @@ const current = ref(gettext.current)
 const languageAvailable = gettext.available
 
 function init() {
-    if (current.value !== 'en') {
-        http.get('/translation/' + current.value).then(r => {
-            gettext.translations[current.value] = r
-        })
-    }
+  if (current.value !== 'en') {
+    http.get('/translation/' + current.value).then(r => {
+      gettext.translations[current.value] = r
+    })
+  }
 }
 
 init()
 
 watch(current, (v) => {
-    init()
-    settings.set_language(v)
-    gettext.current = v
-    // @ts-ignored
-    document.title = route.name() + ' | Nginx UI'
+  init()
+  settings.set_language(v)
+  gettext.current = v
+  // @ts-ignored
+  document.title = route.name() + ' | Nginx UI'
 })
 
 </script>
 
 <template>
-    <div>
-        <a-select v-model:value="current" size="small" style="width: 60px">
-            <a-select-option v-for="(language, key) in languageAvailable" :value="key" :key="key">
-                {{ language }}
-            </a-select-option>
-        </a-select>
-    </div>
+  <div>
+    <a-select v-model:value="current" size="small" style="width: 60px">
+      <a-select-option v-for="(language, key) in languageAvailable" :value="key" :key="key">
+        {{ language }}
+      </a-select-option>
+    </a-select>
+  </div>
 </template>
 
 <style lang="less" scoped>

+ 38 - 38
frontend/src/components/StdDataDisplay/StdBatchEdit.vue

@@ -17,13 +17,13 @@ const visible = ref(false)
 const selectedRowKeys = ref([])
 
 function showModal(c: any, rowKeys: any) {
-    visible.value = true
-    selectedRowKeys.value = rowKeys
-    batchColumns.value = c
+  visible.value = true
+  selectedRowKeys.value = rowKeys
+  batchColumns.value = c
 }
 
 defineExpose({
-    showModal
+  showModal
 })
 
 const data = reactive({})
@@ -31,44 +31,44 @@ const error = reactive({})
 const loading = ref(false)
 
 async function ok() {
-    loading.value = true
-
-    await props.beforeSave?.()
-
-    await props.api(selectedRowKeys.value, data).then(async () => {
-        message.success($gettext('Save successfully'))
-        emit('onSave')
-    }).catch((e: any) => {
-        message.error($gettext(e?.message) ?? $gettext('Server error'))
-    }).finally(() => {
-        loading.value = false
-    })
+  loading.value = true
+
+  await props.beforeSave?.()
+
+  await props.api(selectedRowKeys.value, data).then(async () => {
+    message.success($gettext('Save successfully'))
+    emit('onSave')
+  }).catch((e: any) => {
+    message.error($gettext(e?.message) ?? $gettext('Server error'))
+  }).finally(() => {
+    loading.value = false
+  })
 }
 </script>
 
 <template>
-    <a-modal
-        class="std-curd-edit-modal"
-        :mask="false"
-        :title="$gettext('Batch Modify')"
-        v-model:visible="visible"
-        :cancel-text="$gettext('Cancel')"
-        :ok-text="$gettext('OK')"
-        @ok="ok"
-        :confirm-loading="loading"
-        :width="600"
-        destroyOnClose
-    >
-
-        <std-data-entry
-            ref="std_data_entry"
-            :data-list="batchColumns"
-            :data-source="data"
-            :error="error"
-        />
-
-        <slot name="extra"/>
-    </a-modal>
+  <a-modal
+    class="std-curd-edit-modal"
+    :mask="false"
+    :title="$gettext('Batch Modify')"
+    v-model:open="visible"
+    :cancel-text="$gettext('Cancel')"
+    :ok-text="$gettext('OK')"
+    @ok="ok"
+    :confirm-loading="loading"
+    :width="600"
+    destroyOnClose
+  >
+
+    <std-data-entry
+      ref="std_data_entry"
+      :data-list="batchColumns"
+      :data-source="data"
+      :error="error"
+    />
+
+    <slot name="extra"/>
+  </a-modal>
 </template>
 
 <style scoped>

+ 135 - 135
frontend/src/components/StdDataDisplay/StdCurd.vue

@@ -10,54 +10,54 @@ import {message} from 'ant-design-vue'
 const {$gettext} = gettext
 
 const props = defineProps({
-    api: Object,
-    columns: Array,
-    title: String,
-    data_key: {
-        type: String,
-        default: 'data'
-    },
-    disable_search: {
-        type: Boolean,
-        default: false
-    },
-    disable_add: {
-        type: Boolean,
-        default: false
-    },
-    soft_delete: {
-        type: Boolean,
-        default: false
-    },
-    edit_text: String,
-    deletable: {
-        type: Boolean,
-        default: true
-    },
-    get_params: {
-        type: Object,
-        default() {
-            return {}
-        }
-    },
-    editable: {
-        type: Boolean,
-        default: true
-    },
-    beforeSave: {
-        type: Function,
-        default: () => {
-        }
-    },
-    exportCsv: {
-        type: Boolean,
-        default: false
-    },
-    modalWidth: {
-        type: Number,
-        default: 600
-    },
-    useSortable: Boolean
+  api: Object,
+  columns: Array,
+  title: String,
+  data_key: {
+    type: String,
+    default: 'data'
+  },
+  disable_search: {
+    type: Boolean,
+    default: false
+  },
+  disable_add: {
+    type: Boolean,
+    default: false
+  },
+  soft_delete: {
+    type: Boolean,
+    default: false
+  },
+  edit_text: String,
+  deletable: {
+    type: Boolean,
+    default: true
+  },
+  get_params: {
+    type: Object,
+    default() {
+      return {}
+    }
+  },
+  editable: {
+    type: Boolean,
+    default: true
+  },
+  beforeSave: {
+    type: Function,
+    default: () => {
+    }
+  },
+  exportCsv: {
+    type: Boolean,
+    default: false
+  },
+  modalWidth: {
+    type: Number,
+    default: 600
+  },
+  useSortable: Boolean
 })
 
 const visible = ref(false)
@@ -68,134 +68,134 @@ const error: any = reactive({})
 const selected = ref([])
 
 function onSelect(keys: any) {
-    selected.value = keys
+  selected.value = keys
 }
 
 function editableColumns() {
-    return props.columns!.filter((c: any) => {
-        return c.edit
-    })
+  return props.columns!.filter((c: any) => {
+    return c.edit
+  })
 }
 
 function add() {
-    Object.keys(data).forEach(v => {
-        delete data[v]
-    })
+  Object.keys(data).forEach(v => {
+    delete data[v]
+  })
 
-    clear_error()
-    visible.value = true
+  clear_error()
+  visible.value = true
 }
 
 function get_list() {
-    const t: Table = table.value!
-    t!.get_list()
+  const t: Table = table.value!
+  t!.get_list()
 }
 
 defineExpose({
-    add,
-    get_list,
-    data
+  add,
+  get_list,
+  data
 })
 
 const table = ref(null)
 
 interface Table {
-    get_list(): void
+  get_list(): void
 }
 
 function clear_error() {
-    Object.keys(error).forEach(v => {
-        delete error[v]
-    })
+  Object.keys(error).forEach(v => {
+    delete error[v]
+  })
 }
 
 const ok = async () => {
-    clear_error()
-    await props?.beforeSave!?.(data)
-    props.api!.save(data.id, data).then((r: any) => {
-        message.success($gettext('Save Successfully'))
-        Object.assign(data, r)
-        get_list()
-        visible.value = false
-    }).catch((e: any) => {
-        message.error($gettext(e?.message ?? 'Server error'), 5)
-        Object.assign(error, e.errors)
-    })
+  clear_error()
+  await props?.beforeSave!?.(data)
+  props.api!.save(data.id, data).then((r: any) => {
+    message.success($gettext('Save Successfully'))
+    Object.assign(data, r)
+    get_list()
+    visible.value = false
+  }).catch((e: any) => {
+    message.error($gettext(e?.message ?? 'Server error'), 5)
+    Object.assign(error, e.errors)
+  })
 }
 
 function cancel() {
-    visible.value = false
+  visible.value = false
 
-    clear_error()
+  clear_error()
 }
 
 function edit(id: any) {
-    props.api!.get(id).then(async (r: any) => {
-        Object.keys(data).forEach(k => {
-            delete data[k]
-        })
-        data.id = null
-        Object.assign(data, r)
-        visible.value = true
-    }).catch((e: any) => {
-        message.error($gettext(e?.message ?? 'Server error'), 5)
+  props.api!.get(id).then(async (r: any) => {
+    Object.keys(data).forEach(k => {
+      delete data[k]
     })
+    data.id = null
+    Object.assign(data, r)
+    visible.value = true
+  }).catch((e: any) => {
+    message.error($gettext(e?.message ?? 'Server error'), 5)
+  })
 }
 
 const selectedRowKeys = ref([])
 </script>
 
 <template>
-    <div class="std-curd">
-        <a-card :title="title||$gettext('Table')">
-            <template v-if="!disable_add" #extra>
-                <a @click="add">{{ $gettext('Add') }}</a>
-            </template>
-
-            <std-table
-                ref="table"
-                v-model:selected-row-keys="selectedRowKeys"
-                v-bind="props"
-                @clickEdit="edit"
-                @selected="onSelect"
-                :key="update"
-            >
-                <template v-slot:actions="slotProps">
-                    <slot name="actions" :actions="slotProps.record"/>
-                </template>
-            </std-table>
-        </a-card>
-
-        <a-modal
-            class="std-curd-edit-modal"
-            :mask="false"
-            :title="edit_text?edit_text:(data.id ? $gettext('Modify') : $gettext('Add'))"
-            :visible="visible"
-            :cancel-text="$gettext('Cancel')"
-            :ok-text="$gettext('OK')"
-            @cancel="cancel"
-            @ok="ok"
-            :width="modalWidth"
-            destroyOnClose
-        >
-            <div class="before-edit" v-if="$slots.beforeEdit">
-                <slot name="beforeEdit" :data="data"/>
-            </div>
-
-            <std-data-entry
-                ref="std_data_entry"
-                :data-list="editableColumns()"
-                :data-source="data"
-                :error="error"
-            />
-
-            <slot name="edit" :data="data"/>
-        </a-modal>
-    </div>
+  <div class="std-curd">
+    <a-card :title="title||$gettext('Table')">
+      <template v-if="!disable_add" #extra>
+        <a @click="add">{{ $gettext('Add') }}</a>
+      </template>
+
+      <std-table
+        ref="table"
+        v-model:selected-row-keys="selectedRowKeys"
+        v-bind="props"
+        @clickEdit="edit"
+        @selected="onSelect"
+        :key="update"
+      >
+        <template v-slot:actions="slotProps">
+          <slot name="actions" :actions="slotProps.record"/>
+        </template>
+      </std-table>
+    </a-card>
+
+    <a-modal
+      class="std-curd-edit-modal"
+      :mask="false"
+      :title="edit_text?edit_text:(data.id ? $gettext('Modify') : $gettext('Add'))"
+      :open="visible"
+      :cancel-text="$gettext('Cancel')"
+      :ok-text="$gettext('OK')"
+      @cancel="cancel"
+      @ok="ok"
+      :width="modalWidth"
+      destroyOnClose
+    >
+      <div class="before-edit" v-if="$slots.beforeEdit">
+        <slot name="beforeEdit" :data="data"/>
+      </div>
+
+      <std-data-entry
+        ref="std_data_entry"
+        :data-list="editableColumns()"
+        :data-source="data"
+        :error="error"
+      />
+
+      <slot name="edit" :data="data"/>
+    </a-modal>
+  </div>
 </template>
 
 <style lang="less" scoped>
 :deep(.before-edit:last-child) {
-    margin-bottom: 20px;
+  margin-bottom: 20px;
 }
 </style>

+ 26 - 26
frontend/src/components/StdDataDisplay/StdPagination.vue

@@ -7,47 +7,47 @@ const emit = defineEmits(['change', 'changePageSize'])
 const {$gettext} = useGettext()
 
 function change(num: number, pageSize: number) {
-    emit('change', num, pageSize)
+  emit('change', num, pageSize)
 }
 
 const pageSize = computed({
-    get() {
-        return props.pagination.per_page
-    },
-    set(v) {
-        emit('changePageSize', v)
-        props.pagination.per_page = v
-    }
+  get() {
+    return props.pagination.per_page
+  },
+  set(v) {
+    emit('changePageSize', v)
+    props.pagination.per_page = v
+  }
 })
 </script>
 
 <template>
-    <div class="pagination-container" v-if="pagination.total>pagination.per_page">
-        <a-pagination
-            :current="pagination.current_page"
-            v-model:pageSize="pageSize"
-            :size="size"
-            :total="pagination.total"
-            @change="change"
-        />
-    </div>
+  <div class="pagination-container" v-if="pagination.total>pagination.per_page">
+    <a-pagination
+      :current="pagination.current_page"
+      v-model:pageSize="pageSize"
+      :size="size"
+      :total="pagination.total"
+      @change="change"
+    />
+  </div>
 </template>
 
 <style lang="less">
 .ant-pagination-total-text {
-    @media (max-width: 450px) {
-        display: block;
-    }
+  @media (max-width: 450px) {
+    display: block;
+  }
 }
 </style>
 
 <style lang="less" scoped>
 .pagination-container {
-    padding: 10px 0 0 0;
-    display: flex;
-    justify-content: right;
-    @media (max-width: 450px) {
-        justify-content: center;
-    }
+  padding: 10px 0 0 0;
+  display: flex;
+  justify-content: right;
+  @media (max-width: 450px) {
+    justify-content: center;
+  }
 }
 </style>

+ 421 - 421
frontend/src/components/StdDataDisplay/StdTable.vue

@@ -16,66 +16,66 @@ const {$gettext, interpolate} = gettext
 const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys', 'clickBatchModify'])
 
 const props = defineProps({
-    api: Object,
-    columns: Array,
-    data_key: {
-        type: String,
-        default: 'data'
-    },
-    disable_search: {
-        type: Boolean,
-        default: false
-    },
-    disable_query_params: {
-        type: Boolean,
-        default: false
-    },
-    disable_add: {
-        type: Boolean,
-        default: false
-    },
-    edit_text: String,
-    deletable: {
-        type: Boolean,
-        default: true
-    },
-    get_params: {
-        type: Object,
-        default() {
-            return {}
-        }
-    },
-    editable: {
-        type: Boolean,
-        default: true
-    },
-    selectionType: {
-        type: String,
-        validator: function (value: string) {
-            return ['checkbox', 'radio'].indexOf(value) !== -1
-        }
-    },
-    pithy: {
-        type: Boolean,
-        default: false
-    },
-    scrollX: {
-        type: [Number, Boolean],
-        default: true
-    },
-    rowKey: {
-        type: String,
-        default: 'id'
-    },
-    exportCsv: {
-        type: Boolean,
-        default: false
-    },
-    size: String,
-    selectedRowKeys: {
-        type: Array
-    },
-    useSortable: Boolean
+  api: Object,
+  columns: Array,
+  data_key: {
+    type: String,
+    default: 'data'
+  },
+  disable_search: {
+    type: Boolean,
+    default: false
+  },
+  disable_query_params: {
+    type: Boolean,
+    default: false
+  },
+  disable_add: {
+    type: Boolean,
+    default: false
+  },
+  edit_text: String,
+  deletable: {
+    type: Boolean,
+    default: true
+  },
+  get_params: {
+    type: Object,
+    default() {
+      return {}
+    }
+  },
+  editable: {
+    type: Boolean,
+    default: true
+  },
+  selectionType: {
+    type: String,
+    validator: function (value: string) {
+      return ['checkbox', 'radio'].indexOf(value) !== -1
+    }
+  },
+  pithy: {
+    type: Boolean,
+    default: false
+  },
+  scrollX: {
+    type: [Number, Boolean],
+    default: true
+  },
+  rowKey: {
+    type: String,
+    default: 'id'
+  },
+  exportCsv: {
+    type: Boolean,
+    default: false
+  },
+  size: String,
+  selectedRowKeys: {
+    type: Array
+  },
+  useSortable: Boolean
 })
 
 const data_source: any = ref([])
@@ -84,27 +84,27 @@ const rows_key_index_map: any = ref({})
 
 const loading = ref(true)
 const pagination = reactive({
-    total: 1,
-    per_page: 10,
-    current_page: 1,
-    total_pages: 1
+  total: 1,
+  per_page: 10,
+  current_page: 1,
+  total_pages: 1
 })
 
 const route = useRoute()
 const params = reactive({
-    ...props.get_params
+  ...props.get_params
 })
 
 const selectedKeysLocalBuffer: any = ref([])
 
 const selectedRowKeysBuffer = computed({
-    get() {
-        return props.selectedRowKeys || selectedKeysLocalBuffer.value
-    },
-    set(v) {
-        selectedKeysLocalBuffer.value = v
-        emit('update:selectedRowKeys', v)
-    }
+  get() {
+    return props.selectedRowKeys || selectedKeysLocalBuffer.value
+  },
+  set(v) {
+    selectedKeysLocalBuffer.value = v
+    emit('update:selectedRowKeys', v)
+  }
 })
 
 const searchColumns = getSearchColumns()
@@ -112,472 +112,472 @@ const pithyColumns = getPithyColumns()
 const batchColumns = getBatchEditColumns()
 
 onMounted(() => {
-    if (!props.disable_query_params) {
-        Object.assign(params, route.query)
-    }
-    get_list()
-
-    if (props.useSortable) {
-        initSortable()
-    }
+  if (!props.disable_query_params) {
+    Object.assign(params, route.query)
+  }
+  get_list()
+
+  if (props.useSortable) {
+    initSortable()
+  }
 })
 
 defineExpose({
-    get_list
+  get_list
 })
 
 function destroy(id: any) {
-    props.api!.destroy(id).then(() => {
-        get_list()
-        message.success(interpolate($gettext('Delete ID: %{id}'), {id: id}))
-    }).catch((e: any) => {
-        message.error($gettext(e?.message ?? 'Server error'))
-    })
+  props.api!.destroy(id).then(() => {
+    get_list()
+    message.success(interpolate($gettext('Delete ID: %{id}'), {id: id}))
+  }).catch((e: any) => {
+    message.error($gettext(e?.message ?? 'Server error'))
+  })
 }
 
 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: any) => {
-        data_source.value = r.data
-        rows_key_index_map.value = {}
-        if (props.useSortable) {
-            function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
-                if (data && data.length > 0) {
-                    data.forEach((v: any) => {
-                        v.level = level
-                        let current_index = [...total, index++]
-                        rows_key_index_map.value[v.id] = current_index
-                        if (v.children) buildIndexMap(v.children, level + 1, 0, current_index)
-                    })
-                }
-            }
-
-            buildIndexMap(r.data)
+  loading.value = true
+  if (page_num) {
+    params['page'] = page_num
+    params['page_size'] = page_size
+  }
+  props.api!.get_list(params).then(async (r: any) => {
+    data_source.value = r.data
+    rows_key_index_map.value = {}
+    if (props.useSortable) {
+      function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
+        if (data && data.length > 0) {
+          data.forEach((v: any) => {
+            v.level = level
+            let current_index = [...total, index++]
+            rows_key_index_map.value[v.id] = current_index
+            if (v.children) buildIndexMap(v.children, level + 1, 0, current_index)
+          })
         }
+      }
 
-        if (r.pagination !== undefined) {
-            Object.assign(pagination, r.pagination)
-        }
+      buildIndexMap(r.data)
+    }
 
-        loading.value = false
-    }).catch((e: any) => {
-        message.error(e?.message ?? $gettext('Server error'))
-    })
+    if (r.pagination !== undefined) {
+      Object.assign(pagination, r.pagination)
+    }
+
+    loading.value = false
+  }).catch((e: any) => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
 }
 
 function stdChange(pagination: any, filters: any, sorter: any) {
-    if (sorter) {
-        selectedRowKeysBuffer.value = []
-        params['order_by'] = sorter.field
-        params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
-        switch (sorter.order) {
-            case 'ascend':
-                params['sort'] = 'asc'
-                break
-            case 'descend':
-                params['sort'] = 'desc'
-                break
-            default:
-                params['sort'] = null
-                break
-        }
-    }
-    if (pagination) {
-        selectedRowKeysBuffer.value = []
+  if (sorter) {
+    selectedRowKeysBuffer.value = []
+    params['order_by'] = sorter.field
+    params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
+    switch (sorter.order) {
+      case 'ascend':
+        params['sort'] = 'asc'
+        break
+      case 'descend':
+        params['sort'] = 'desc'
+        break
+      default:
+        params['sort'] = null
+        break
     }
+  }
+  if (pagination) {
+    selectedRowKeysBuffer.value = []
+  }
 }
 
 function expandedTable(keys: any) {
-    expand_keys_list.value = keys
+  expand_keys_list.value = keys
 }
 
 function getSearchColumns() {
-    let searchColumns: any = []
-    props.columns!.forEach((column: any) => {
-        if (column.search) {
-            searchColumns.push(column)
-        }
-    })
-    return searchColumns
+  let searchColumns: any = []
+  props.columns!.forEach((column: any) => {
+    if (column.search) {
+      searchColumns.push(column)
+    }
+  })
+  return searchColumns
 }
 
 function getBatchEditColumns() {
-    let batch: any = []
-    props.columns!.forEach((column: any) => {
-        if (column.batch) {
-            batch.push(column)
-        }
-    })
-    return batch
+  let batch: any = []
+  props.columns!.forEach((column: any) => {
+    if (column.batch) {
+      batch.push(column)
+    }
+  })
+  return batch
 }
 
 function getPithyColumns() {
-    if (props.pithy) {
-        return props.columns!.filter((c: any, index: any, columns: any) => {
-            return c.pithy === true && c.display !== false
-        })
-    }
+  if (props.pithy) {
     return props.columns!.filter((c: any, index: any, columns: any) => {
-        return c.display !== false
+      return c.pithy === true && c.display !== false
     })
+  }
+  return props.columns!.filter((c: any, index: any, columns: any) => {
+    return c.display !== false
+  })
 }
 
 function checked(c: any) {
-    params[c.target.value] = c.target.checked
+  params[c.target.value] = c.target.checked
 }
 
 const crossPageSelect: any = {}
 
 async function onSelectChange(_selectedRowKeys: any) {
-    const page = params.page || 1
-
-    crossPageSelect[page] = await _selectedRowKeys
-
-    let t: any = []
-    Object.keys(crossPageSelect).forEach(v => {
-        t.push(...crossPageSelect[v])
-    })
-    const n: any = [..._selectedRowKeys]
-    t = await t.concat(n)
-    // console.log(crossPageSelect)
-    const set = new Set(t)
-    selectedRowKeysBuffer.value = Array.from(set)
-    emit('onSelected', selectedRowKeysBuffer.value)
+  const page = params.page || 1
+
+  crossPageSelect[page] = await _selectedRowKeys
+
+  let t: any = []
+  Object.keys(crossPageSelect).forEach(v => {
+    t.push(...crossPageSelect[v])
+  })
+  const n: any = [..._selectedRowKeys]
+  t = await t.concat(n)
+  // console.log(crossPageSelect)
+  const set = new Set(t)
+  selectedRowKeysBuffer.value = Array.from(set)
+  emit('onSelected', selectedRowKeysBuffer.value)
 }
 
 function onSelect(record: any) {
-    emit('onSelectedRecord', record)
+  emit('onSelectedRecord', record)
 }
 
 const router = useRouter()
 
 const reset_search = async () => {
-    Object.keys(params).forEach(v => {
-        delete params[v]
-    })
+  Object.keys(params).forEach(v => {
+    delete params[v]
+  })
 
-    Object.assign(params, {
-        ...props.get_params
-    })
+  Object.assign(params, {
+    ...props.get_params
+  })
 
-    router.push({query: {}}).catch(() => {
-    })
+  router.push({query: {}}).catch(() => {
+  })
 }
 
 watch(params, () => {
-    if (!props.disable_query_params) {
-        router.push({query: params})
-    }
-    get_list()
+  if (!props.disable_query_params) {
+    router.push({query: params})
+  }
+  get_list()
 })
 
 const rowSelection = computed(() => {
-    if (batchColumns.length > 0 || props.selectionType) {
-        return {
-            selectedRowKeys: selectedRowKeysBuffer.value, onChange: onSelectChange,
-            onSelect: onSelect, type: batchColumns.length > 0 ? 'checkbox' : props.selectionType
-        }
-    } else {
-        return null
+  if (batchColumns.length > 0 || props.selectionType) {
+    return {
+      selectedRowKeys: selectedRowKeysBuffer.value, onChange: onSelectChange,
+      onSelect: onSelect, type: batchColumns.length > 0 ? 'checkbox' : props.selectionType
     }
+  } else {
+    return null
+  }
 })
 
 function fn(obj: Object, desc: string) {
-    const arr: string[] = desc.split('.')
-    while (arr.length) {
-        // @ts-ignore
-        const top = obj[arr.shift()]
-        if (top === undefined) {
-            return null
-        }
-        obj = top
+  const arr: string[] = desc.split('.')
+  while (arr.length) {
+    // @ts-ignore
+    const top = obj[arr.shift()]
+    if (top === undefined) {
+      return null
     }
-    return obj
+    obj = top
+  }
+  return obj
 }
 
 async function export_csv() {
-    let header = []
-    let headerKeys: any[] = []
-    const showColumnsMap: any = {}
+  let header = []
+  let headerKeys: any[] = []
+  const showColumnsMap: any = {}
+  // @ts-ignore
+  for (let showColumnsKey in pithyColumns) {
     // @ts-ignore
-    for (let showColumnsKey in pithyColumns) {
-        // @ts-ignore
-        if (pithyColumns[showColumnsKey].dataIndex === 'action') continue
-        // @ts-ignore
-        let t = pithyColumns[showColumnsKey].title
-
-        if (typeof t === 'function') {
-            t = t()
-        }
-        header.push({
-            title: t,
-            // @ts-ignore
-            key: pithyColumns[showColumnsKey].dataIndex
-        })
-        // @ts-ignore
-        headerKeys.push(pithyColumns[showColumnsKey].dataIndex)
-        // @ts-ignore
-        showColumnsMap[pithyColumns[showColumnsKey].dataIndex] = pithyColumns[showColumnsKey]
-    }
+    if (pithyColumns[showColumnsKey].dataIndex === 'action') continue
+    // @ts-ignore
+    let t = pithyColumns[showColumnsKey].title
 
-    let dataSource: any = []
-    let hasMore = true
-    let page = 1
-    while (hasMore) {
-        // 准备 DataSource
-        await props.api!.get_list({page}).then((response: any) => {
-            if (response.data.length === 0) {
-                hasMore = false
-                return
-            }
-            if (response[props.data_key] === undefined) {
-                dataSource = dataSource.concat(...response.data)
-            } else {
-                dataSource = dataSource.concat(...response[props.data_key])
-            }
-        }).catch((e: any) => {
-            message.error(e.message ?? $gettext('Server error'))
-            hasMore = false
-            return
-        })
-        page += 1
+    if (typeof t === 'function') {
+      t = t()
     }
-    const data: any[] = []
-    dataSource.forEach((row: Object) => {
-        let obj: any = {}
-        headerKeys.forEach(key => {
-            let data = fn(row, key)
-            const c = showColumnsMap[key]
-            data = c?.customRender?.({text: data}) ?? data
-            obj[c.dataIndex] = data
-        })
-        data.push(obj)
+    header.push({
+      title: t,
+      // @ts-ignore
+      key: pithyColumns[showColumnsKey].dataIndex
+    })
+    // @ts-ignore
+    headerKeys.push(pithyColumns[showColumnsKey].dataIndex)
+    // @ts-ignore
+    showColumnsMap[pithyColumns[showColumnsKey].dataIndex] = pithyColumns[showColumnsKey]
+  }
+
+  let dataSource: any = []
+  let hasMore = true
+  let page = 1
+  while (hasMore) {
+    // 准备 DataSource
+    await props.api!.get_list({page}).then((response: any) => {
+      if (response.data.length === 0) {
+        hasMore = false
+        return
+      }
+      if (response[props.data_key] === undefined) {
+        dataSource = dataSource.concat(...response.data)
+      } else {
+        dataSource = dataSource.concat(...response[props.data_key])
+      }
+    }).catch((e: any) => {
+      message.error(e.message ?? $gettext('Server error'))
+      hasMore = false
+      return
+    })
+    page += 1
+  }
+  const data: any[] = []
+  dataSource.forEach((row: Object) => {
+    let obj: any = {}
+    headerKeys.forEach(key => {
+      let data = fn(row, key)
+      const c = showColumnsMap[key]
+      data = c?.customRender?.({text: data}) ?? data
+      obj[c.dataIndex] = data
     })
+    data.push(obj)
+  })
 
-    downloadCsv(header, data,
-        `${$gettext('Export')}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
+  downloadCsv(header, data,
+    `${$gettext('Export')}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
 }
 
 const hasSelectedRow = computed(() => {
-    return batchColumns.length > 0 && selectedRowKeysBuffer.value.length > 0
+  return batchColumns.length > 0 && selectedRowKeysBuffer.value.length > 0
 })
 
 function click_batch_edit() {
-    emit('clickBatchModify', batchColumns, selectedRowKeysBuffer.value)
+  emit('clickBatchModify', batchColumns, selectedRowKeysBuffer.value)
 }
 
 function getLeastIndex(index: number) {
-    return index >= 1 ? index : 1
+  return index >= 1 ? index : 1
 }
 
 function getTargetData(data: any, indexList: number[]): any {
-    let target: any = {children: data}
-    indexList.forEach((index: number) => {
-        target.children[index].parent = target
-        target = target.children[index]
-    })
-    return target
+  let target: any = {children: data}
+  indexList.forEach((index: number) => {
+    target.children[index].parent = target
+    target = target.children[index]
+  })
+  return target
 }
 
 function initSortable() {
-    const table: any = document.querySelector('#std-table tbody')
-    new Sortable(table, {
-        handle: '.ant-table-drag-icon',
-        animation: 150,
-        sort: true,
-        forceFallback: true,
-        setData: function (dataTransfer) {
-            dataTransfer.setData('Text', '')
-        },
-        onStart({item}) {
-            let targetRowKey = Number(item.dataset.rowKey)
-            if (targetRowKey) {
-                expand_keys_list.value = expand_keys_list.value.filter((item: number) => item !== targetRowKey)
-            }
-        },
-        onMove({dragged, related}) {
-            const oldRow: number[] = rows_key_index_map.value?.[Number(dragged.dataset.rowKey)]
-            const newRow: number[] = rows_key_index_map.value?.[Number(related.dataset.rowKey)]
-            if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] != newRow[newRow.length - 2]) {
-                return false
-            }
-        },
-        async onEnd({item, newIndex, oldIndex}) {
-            if (newIndex === oldIndex) return
-
-            const indexDelta: number = Number(oldIndex) - Number(newIndex)
-            const direction: number = indexDelta > 0 ? +1 : -1
-
-            let rowIndex: number[] = rows_key_index_map.value?.[Number(item.dataset.rowKey)]
-            const newRow = getTargetData(data_source.value, rowIndex)
-            const newRowParent = newRow.parent
-            const level: number = newRow.level
-
-            let currentRowIndex: number[] = [...rows_key_index_map.value?.
-                [Number(table.children[Number(newIndex) + direction].dataset.rowKey)]]
-            let currentRow: any = getTargetData(data_source.value, currentRowIndex)
-            // Reset parent
-            currentRow.parent = newRow.parent = null
-            newRowParent.children.splice(rowIndex[level], 1)
-            newRowParent.children.splice(currentRowIndex[level], 0, toRaw(newRow))
-
-            let changeIds: number[] = []
-
-            function processChanges(row: any, children: boolean = false, newIndex: number | undefined = undefined) {
-                // Build changes ID list expect new row
-                if (children || newIndex === undefined) changeIds.push(row.id)
-
-                if (newIndex !== undefined)
-                    rows_key_index_map.value[row.id][level] = newIndex
-                else if (children)
-                    rows_key_index_map.value[row.id][level] += direction
-
-                row.parent = null
-                if (row.children) {
-                    row.children.forEach((v: any) => processChanges(v, true, newIndex))
-                }
-            }
-
-            // Replace row index for new row
-            processChanges(newRow, false, currentRowIndex[level])
-            // Rebuild row index maps for changes row
-            for (let i = Number(oldIndex); i != newIndex; i -= direction) {
-                let rowIndex: number[] = rows_key_index_map.value?.[table.children[i].dataset.rowKey]
-                rowIndex[level] += direction
-                processChanges(getTargetData(data_source.value, rowIndex))
-            }
-            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,
-                direction: direction,
-                affected_ids: changeIds
-            }).then(() => {
-                message.success($gettext('Updated successfully'))
-            }).catch((e: any) => {
-                message.error(e?.message ?? $gettext('Server error'))
-            })
+  const table: any = document.querySelector('#std-table tbody')
+  new Sortable(table, {
+    handle: '.ant-table-drag-icon',
+    animation: 150,
+    sort: true,
+    forceFallback: true,
+    setData: function (dataTransfer) {
+      dataTransfer.setData('Text', '')
+    },
+    onStart({item}) {
+      let targetRowKey = Number(item.dataset.rowKey)
+      if (targetRowKey) {
+        expand_keys_list.value = expand_keys_list.value.filter((item: number) => item !== targetRowKey)
+      }
+    },
+    onMove({dragged, related}) {
+      const oldRow: number[] = rows_key_index_map.value?.[Number(dragged.dataset.rowKey)]
+      const newRow: number[] = rows_key_index_map.value?.[Number(related.dataset.rowKey)]
+      if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] != newRow[newRow.length - 2]) {
+        return false
+      }
+    },
+    async onEnd({item, newIndex, oldIndex}) {
+      if (newIndex === oldIndex) return
+
+      const indexDelta: number = Number(oldIndex) - Number(newIndex)
+      const direction: number = indexDelta > 0 ? +1 : -1
+
+      let rowIndex: number[] = rows_key_index_map.value?.[Number(item.dataset.rowKey)]
+      const newRow = getTargetData(data_source.value, rowIndex)
+      const newRowParent = newRow.parent
+      const level: number = newRow.level
+
+      let currentRowIndex: number[] = [...rows_key_index_map.value?.
+        [Number(table.children[Number(newIndex) + direction].dataset.rowKey)]]
+      let currentRow: any = getTargetData(data_source.value, currentRowIndex)
+      // Reset parent
+      currentRow.parent = newRow.parent = null
+      newRowParent.children.splice(rowIndex[level], 1)
+      newRowParent.children.splice(currentRowIndex[level], 0, toRaw(newRow))
+
+      let changeIds: number[] = []
+
+      function processChanges(row: any, children: boolean = false, newIndex: number | undefined = undefined) {
+        // Build changes ID list expect new row
+        if (children || newIndex === undefined) changeIds.push(row.id)
+
+        if (newIndex !== undefined)
+          rows_key_index_map.value[row.id][level] = newIndex
+        else if (children)
+          rows_key_index_map.value[row.id][level] += direction
+
+        row.parent = null
+        if (row.children) {
+          row.children.forEach((v: any) => processChanges(v, true, newIndex))
         }
-    })
+      }
+
+      // Replace row index for new row
+      processChanges(newRow, false, currentRowIndex[level])
+      // Rebuild row index maps for changes row
+      for (let i = Number(oldIndex); i != newIndex; i -= direction) {
+        let rowIndex: number[] = rows_key_index_map.value?.[table.children[i].dataset.rowKey]
+        rowIndex[level] += direction
+        processChanges(getTargetData(data_source.value, rowIndex))
+      }
+      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,
+        direction: direction,
+        affected_ids: changeIds
+      }).then(() => {
+        message.success($gettext('Updated successfully'))
+      }).catch((e: any) => {
+        message.error(e?.message ?? $gettext('Server error'))
+      })
+    }
+  })
 }
 
 
 </script>
 
 <template>
-    <div class="std-table">
-        <std-data-entry
-            v-if="!disable_search && searchColumns.length"
-            :data-list="searchColumns"
-            :data-source="params"
-            layout="inline"
-        >
-            <template #action>
-                <a-space class="action-btn">
-                    <a-button v-if="exportCsv" @click="export_csv" type="primary" ghost>
-                        {{ $gettext('Export') }}
-                    </a-button>
-                    <a-button @click="reset_search">
-                        {{ $gettext('Reset') }}
-                    </a-button>
-                    <a-button v-if="hasSelectedRow" @click="click_batch_edit">
-                        {{ $gettext('Batch Modify') }}
-                    </a-button>
-                </a-space>
-            </template>
-        </std-data-entry>
-        <a-table
-            :columns="pithyColumns"
-            :data-source="data_source"
-            :loading="loading"
-            :pagination="false"
-            :row-key="rowKey"
-            :rowSelection="rowSelection"
-            @change="stdChange"
-            :scroll="{ x: scrollX }"
-            :size="size"
-            id="std-table"
-            @expandedRowsChange="expandedTable"
-            :expandedRowKeys="expand_keys_list"
-        >
-            <template
-                v-slot:bodyCell="{text, record, index, column}"
-            >
-                <template v-if="column.handle === true">
-                    <span class="ant-table-drag-icon"><HolderOutlined/></span>
-                    {{ text }}
-                </template>
-                <template v-if="column.dataIndex === 'action'">
-                    <a-button type="link" size="small" v-if="props.editable"
-                              @click="$emit('clickEdit', record[props.rowKey], record)">
-                        {{ props.edit_text || $gettext('Modify') }}
-                    </a-button>
-                    <slot name="actions" :record="record"/>
-                    <template v-if="props.deletable">
-                        <a-divider type="vertical"/>
-                        <a-popconfirm
-                            :cancelText="$gettext('No')"
-                            :okText="$gettext('OK')"
-                            :title="$gettext('Are you sure you want to delete?')"
-                            @confirm="destroy(record[rowKey])">
-                            <a-button type="link" size="small">{{ $gettext('Delete') }}</a-button>
-                        </a-popconfirm>
-                    </template>
-                </template>
-            </template>
-        </a-table>
-        <std-pagination :size="size" :pagination="pagination" @change="get_list" @changePageSize="stdChange"/>
-    </div>
+  <div class="std-table">
+    <std-data-entry
+      v-if="!disable_search && searchColumns.length"
+      :data-list="searchColumns"
+      :data-source="params"
+      layout="inline"
+    >
+      <template #action>
+        <a-space class="action-btn">
+          <a-button v-if="exportCsv" @click="export_csv" type="primary" ghost>
+            {{ $gettext('Export') }}
+          </a-button>
+          <a-button @click="reset_search">
+            {{ $gettext('Reset') }}
+          </a-button>
+          <a-button v-if="hasSelectedRow" @click="click_batch_edit">
+            {{ $gettext('Batch Modify') }}
+          </a-button>
+        </a-space>
+      </template>
+    </std-data-entry>
+    <a-table
+      :columns="pithyColumns"
+      :data-source="data_source"
+      :loading="loading"
+      :pagination="false"
+      :row-key="rowKey"
+      :rowSelection="rowSelection"
+      @change="stdChange"
+      :scroll="{ x: scrollX }"
+      :size="size"
+      id="std-table"
+      @expandedRowsChange="expandedTable"
+      :expandedRowKeys="expand_keys_list"
+    >
+      <template
+        v-slot:bodyCell="{text, record, index, column}"
+      >
+        <template v-if="column.handle === true">
+          <span class="ant-table-drag-icon"><HolderOutlined/></span>
+          {{ text }}
+        </template>
+        <template v-if="column.dataIndex === 'action'">
+          <a-button type="link" size="small" v-if="props.editable"
+                    @click="$emit('clickEdit', record[props.rowKey], record)">
+            {{ props.edit_text || $gettext('Modify') }}
+          </a-button>
+          <slot name="actions" :record="record"/>
+          <template v-if="props.deletable">
+            <a-divider type="vertical"/>
+            <a-popconfirm
+              :cancelText="$gettext('No')"
+              :okText="$gettext('OK')"
+              :title="$gettext('Are you sure you want to delete?')"
+              @confirm="destroy(record[rowKey])">
+              <a-button type="link" size="small">{{ $gettext('Delete') }}</a-button>
+            </a-popconfirm>
+          </template>
+        </template>
+      </template>
+    </a-table>
+    <std-pagination :size="size" :pagination="pagination" @change="get_list" @changePageSize="stdChange"/>
+  </div>
 </template>
 
 <style lang="less">
 .ant-table-scroll {
-    .ant-table-body {
-        overflow-x: auto !important;
-    }
+  .ant-table-body {
+    overflow-x: auto !important;
+  }
 }
 </style>
 
 <style lang="less" scoped>
 .ant-form {
-    margin: 10px 0 20px 0;
+  margin: 10px 0 20px 0;
 }
 
 .ant-slider {
-    min-width: 90px;
+  min-width: 90px;
 }
 
 .std-table {
-    .ant-table-wrapper {
-        // overflow-x: scroll;
-    }
+  .ant-table-wrapper {
+    // overflow-x: scroll;
+  }
 }
 
 .action-btn {
-    // min-height: 50px;
-    height: 100%;
-    display: flex;
-    align-items: flex-start;
+  // min-height: 50px;
+  height: 100%;
+  display: flex;
+  align-items: flex-start;
 }
 
 :deep(.ant-form-inline .ant-form-item) {
-    margin-bottom: 10px;
+  margin-bottom: 10px;
 }
 </style>
 
 <style lang="less">
 .ant-table-drag-icon {
-    float: left;
-    margin-right: 16px;
-    cursor: grab;
+  float: left;
+  margin-right: 16px;
+  cursor: grab;
 }
 
 .sortable-ghost *, .sortable-chosen * {
-    cursor: grabbing !important;
+  cursor: grabbing !important;
 }
 </style>

+ 17 - 17
frontend/src/components/StdDataDisplay/StdTableTransformer.tsx

@@ -2,31 +2,31 @@
 import dayjs from 'dayjs'
 
 export interface customRender {
-    value: any
-    text: any
-    record: any
-    index: any
-    column: any
+  value: any
+  text: any
+  record: any
+  index: any
+  column: any
 }
 
 export const datetime = (args: customRender) => {
-    return dayjs(args.text).format('YYYY-MM-DD HH:mm:ss')
+  return dayjs(args.text).format('YYYY-MM-DD HH:mm:ss')
 }
 
 export const date = (args: customRender) => {
-    return dayjs(args.text).format('YYYY-MM-DD')
+  return dayjs(args.text).format('YYYY-MM-DD')
 }
 
 export const mask = (args: customRender, maskObj: any) => {
-    let v
+  let v
 
-    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
-    }
+  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 <div>{v}</div>
-}
+  return <div>{v}</div>
+}

+ 27 - 27
frontend/src/components/StdDataEntry/StdDataEntry.tsx

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

+ 18 - 18
frontend/src/components/StdDataEntry/StdFormItem.vue

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

+ 27 - 27
frontend/src/components/StdDataEntry/components/StdPassword.vue

@@ -5,47 +5,47 @@ const props = defineProps(['value', 'generate', 'placeholder'])
 const emit = defineEmits(['update:value'])
 
 const M_value = computed({
-    get() {
-        return props.value
-    },
-    set(v) {
-        emit('update:value', v)
-    }
+  get() {
+    return props.value
+  },
+  set(v) {
+    emit('update:value', v)
+  }
 })
 const visibility = ref(false)
 
 function handle_generate() {
-    visibility.value = true
-    M_value.value = 'xxxx'
+  visibility.value = true
+  M_value.value = 'xxxx'
 
-    const chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-    const passwordLength = 12
-    let password = ''
-    for (let i = 0; i <= passwordLength; i++) {
-        const randomNumber = Math.floor(Math.random() * chars.length)
-        password += chars.substring(randomNumber, randomNumber + 1)
-    }
+  const chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+  const passwordLength = 12
+  let password = ''
+  for (let i = 0; i <= passwordLength; i++) {
+    const randomNumber = Math.floor(Math.random() * chars.length)
+    password += chars.substring(randomNumber, randomNumber + 1)
+  }
 
-    M_value.value = password
+  M_value.value = password
 
 }
 </script>
 
 <template>
-    <a-input-group compact>
-        <a-input-password
-            v-if="!visibility"
-            :class="{compact: generate}"
-            v-model:value="M_value" :placeholoder="placeholder"/>
-        <a-input v-else :class="{compact: generate}" v-model:value="M_value" :placeholoder="placeholder"/>
-        <a-button @click="handle_generate" v-if="generate" type="primary">
-            <translate>Generate</translate>
-        </a-button>
-    </a-input-group>
+  <a-input-group compact>
+    <a-input-password
+      v-if="!visibility"
+      :class="{compact: generate}"
+      v-model:value="M_value" :placeholoder="placeholder"/>
+    <a-input v-else :class="{compact: generate}" v-model:value="M_value" :placeholoder="placeholder"/>
+    <a-button @click="handle_generate" v-if="generate" type="primary">
+      <translate>Generate</translate>
+    </a-button>
+  </a-input-group>
 </template>
 
 <style scoped>
 .compact {
-    width: calc(100% - 91px)
+  width: calc(100% - 91px)
 }
 </style>

+ 23 - 23
frontend/src/components/StdDataEntry/components/StdSelect.vue

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

+ 91 - 91
frontend/src/components/StdDataEntry/components/StdSelector.vue

@@ -5,14 +5,14 @@ import gettext from '@/gettext'
 
 const {$gettext} = gettext
 const props = defineProps(['selectedKey', 'value', 'recordValueIndex',
-    'selectionType', 'api', 'columns', 'data_key',
-    'disable_search', 'get_params', 'description'])
+  'selectionType', 'api', 'columns', 'data_key',
+  'disable_search', 'get_params', 'description'])
 const emit = defineEmits(['update:selectedKey', 'changeSelect'])
 const visible = ref(false)
 const M_value = ref('')
 
 onMounted(() => {
-    init()
+  init()
 })
 
 const selected = ref([])
@@ -20,122 +20,122 @@ const selected = ref([])
 const record: any = reactive({})
 
 function init() {
-    if (props.selectedKey && !props.value && props.selectionType === 'radio') {
-        props.api.get(props.selectedKey).then((r: any) => {
-            Object.assign(record, r)
-            M_value.value = r[props.recordValueIndex]
-        })
-    }
+  if (props.selectedKey && !props.value && props.selectionType === 'radio') {
+    props.api.get(props.selectedKey).then((r: any) => {
+      Object.assign(record, r)
+      M_value.value = r[props.recordValueIndex]
+    })
+  }
 }
 
 function show() {
-    visible.value = true
+  visible.value = true
 }
 
 function onSelect(_selected: any) {
-    selected.value = _selected
+  selected.value = _selected
 }
 
 function onSelectedRecord(r: any) {
-    Object.assign(record, r)
+  Object.assign(record, r)
 }
 
 function ok() {
-    visible.value = false
-    if (props.selectionType == 'radio') {
-        emit('update:selectedKey', selected.value[0])
-    } else {
-        emit('update:selectedKey', selected.value)
-    }
-    M_value.value = record[props.recordValueIndex]
-    emit('changeSelect', record)
+  visible.value = false
+  if (props.selectionType == 'radio') {
+    emit('update:selectedKey', selected.value[0])
+  } else {
+    emit('update:selectedKey', selected.value)
+  }
+  M_value.value = record[props.recordValueIndex]
+  emit('changeSelect', record)
 }
 
 watch(props, () => {
-    if (!props?.selectedKey) {
-        M_value.value = ''
-    } else if (props.value) {
-        M_value.value = props.value
-    } else {
-        init()
-    }
+  if (!props?.selectedKey) {
+    M_value.value = ''
+  } else if (props.value) {
+    M_value.value = props.value
+  } else {
+    init()
+  }
 })
 
 const _selectedKey = computed({
-    get() {
-        return props.selectedKey
-    },
-    set(v) {
-        emit('update:selectedKey', v)
-    }
+  get() {
+    return props.selectedKey
+  },
+  set(v) {
+    emit('update:selectedKey', v)
+  }
 })
 </script>
 
 <template>
-    <div class="std-selector-container">
-        <div class="std-selector" @click="show()">
-            <a-input v-model="_selectedKey" disabled hidden/>
-            <div class="value">
-                {{ M_value }}
-            </div>
-            <a-modal
-                :mask="false"
-                :visible="visible"
-                :cancel-text="$gettext('Cancel')"
-                :ok-text="$gettext('OK')"
-                :title="$gettext('Selector')"
-                @cancel="visible=false"
-                @ok="ok()"
-                :width="800"
-                destroyOnClose
-            >
-                {{ description }}
-                <std-table
-                    :api="api"
-                    :columns="columns"
-                    :data_key="data_key"
-                    :disable_search="disable_search"
-                    :pithy="true"
-                    :get_params="get_params"
-                    :selectionType="selectionType"
-                    :disable_query_params="true"
-                    @onSelected="onSelect"
-                    @onSelectedRecord="onSelectedRecord"
-                />
-            </a-modal>
-        </div>
+  <div class="std-selector-container">
+    <div class="std-selector" @click="show()">
+      <a-input v-model="_selectedKey" disabled hidden/>
+      <div class="value">
+        {{ M_value }}
+      </div>
+      <a-modal
+        :mask="false"
+        :open="visible"
+        :cancel-text="$gettext('Cancel')"
+        :ok-text="$gettext('OK')"
+        :title="$gettext('Selector')"
+        @cancel="visible=false"
+        @ok="ok()"
+        :width="800"
+        destroyOnClose
+      >
+        {{ description }}
+        <std-table
+          :api="api"
+          :columns="columns"
+          :data_key="data_key"
+          :disable_search="disable_search"
+          :pithy="true"
+          :get_params="get_params"
+          :selectionType="selectionType"
+          :disable_query_params="true"
+          @onSelected="onSelect"
+          @onSelectedRecord="onSelectedRecord"
+        />
+      </a-modal>
     </div>
+  </div>
 </template>
 
 <style lang="less" scoped>
 .dark .std-selector-container
 .std-selector-container {
-    height: 39.9px;
-    display: flex;
-    align-items: flex-start;
-
-    .std-selector {
-        box-sizing: border-box;
-        font-variant: tabular-nums;
-        list-style: none;
-        font-feature-settings: 'tnum';
-        height: 32px;
-        padding: 4px 11px;
-        color: rgba(0, 0, 0, 0.85);
-        font-size: 14px;
-        line-height: 1.5;
-        background-color: #fff;
-        background-image: none;
-        border: 1px solid #d9d9d9;
-        border-radius: 4px;
-        transition: all 0.3s;
-        margin: 0 10px 0 0;
-        cursor: pointer;
-        min-width: 180px;
-
-        .value {
-
-        }
+  height: 39.9px;
+  display: flex;
+  align-items: flex-start;
+
+  .std-selector {
+    box-sizing: border-box;
+    font-variant: tabular-nums;
+    list-style: none;
+    font-feature-settings: 'tnum';
+    height: 32px;
+    padding: 4px 11px;
+    color: rgba(0, 0, 0, 0.85);
+    font-size: 14px;
+    line-height: 1.5;
+    background-color: #fff;
+    background-image: none;
+    border: 1px solid #d9d9d9;
+    border-radius: 4px;
+    transition: all 0.3s;
+    margin: 0 10px 0 0;
+    cursor: pointer;
+    min-width: 180px;
+
+    .value {
+
     }
+  }
 }
 </style>

+ 90 - 90
frontend/src/components/StdDataEntry/index.tsx

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

+ 5 - 5
frontend/src/components/StdDataEntry/style.less

@@ -1,7 +1,7 @@
 .std-data-entry-action {
-  @media (max-width: 375px) {
-    display: block;
-    width: 100%;
-    margin: 10px 0;
-  }
+    @media (max-width: 375px) {
+        display: block;
+        width: 100%;
+        margin: 10px 0;
+    }
 }

+ 67 - 0
frontend/src/components/SwitchAppearance/SwitchAppearance.vue

@@ -0,0 +1,67 @@
+<script lang="ts" setup>
+import {computed, inject, Ref} from 'vue'
+import VPSwitch from '@/components/VPSwitch/VPSwitch.vue'
+import VPIconMoon from './icons/VPIconMoon.vue'
+import VPIconSun from './icons/VPIconSun.vue'
+import {useSettingsStore} from '@/pinia'
+import {useGettext} from 'vue3-gettext'
+
+const {$gettext} = useGettext()
+
+const settings = useSettingsStore()
+const devicePrefersTheme = inject('devicePrefersTheme') as Ref<string>
+const isDark = computed(() => settings.theme === 'dark')
+
+const switchTitle = computed(() => {
+  return isDark.value ? $gettext('Switch to light theme') : $gettext('Switch to dark theme')
+})
+
+async function toggleAppearance() {
+  if (isDark.value) {
+    settings.set_theme('light')
+  } else {
+    settings.set_theme('dark')
+  }
+
+  if (devicePrefersTheme.value === settings.theme) {
+    settings.set_preference_theme('auto')
+  } else {
+    settings.set_preference_theme(settings.theme)
+  }
+}
+</script>
+
+<template>
+  <VPSwitch
+    :title="switchTitle"
+    class="VPSwitchAppearance"
+    :aria-checked="isDark"
+    @click="toggleAppearance"
+  >
+    <VPIconSun class="sun"/>
+    <VPIconMoon class="moon"/>
+  </VPSwitch>
+</template>
+
+<style scoped>
+.sun {
+  opacity: 1;
+}
+
+.moon {
+  opacity: 0;
+}
+
+.dark .sun {
+  opacity: 0;
+}
+
+.dark .moon {
+  opacity: 1;
+}
+
+.dark .VPSwitchAppearance :deep(.check) {
+  /*rtl:ignore*/
+  transform: translateX(18px);
+}
+</style>

+ 6 - 0
frontend/src/components/SwitchAppearance/icons/VPIconMoon.vue

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

+ 18 - 0
frontend/src/components/SwitchAppearance/icons/VPIconSun.vue

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

+ 92 - 0
frontend/src/components/VPSwitch/VPSwitch.vue

@@ -0,0 +1,92 @@
+<template>
+  <button class="VPSwitch" type="button" role="switch">
+    <span class="check">
+      <span class="icon" v-if="$slots.default">
+        <slot/>
+      </span>
+    </span>
+  </button>
+</template>
+
+<style lang="less">
+.light {
+  --vp-c-border: #c2c2c4;
+  --vp-c-gray-soft: rgba(142, 150, 170, 0.14);
+  --vp-c-indigo-1: #3451b2;
+  --vp-c-white: #ffffff;
+  --vp-c-black: #000000;
+  --vp-c-text-1: rgba(60, 60, 67);
+  --vp-c-text-2: rgba(60, 60, 67, 0.78);
+  --vp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
+  --vp-c-text-3: rgba(235, 235, 245, 0.38);
+}
+
+.dark {
+  --vp-c-border: #3c3f44;
+  --vp-c-gray-soft: rgba(101, 117, 133, 0.16);
+  --vp-c-indigo-1: #a8b1ff;
+  --vp-c-neutral: var(--vp-c-white);
+  --vp-c-neutral-inverse: var(--vp-c-black);
+  --vp-c-text-1: rgba(255, 255, 245, 0.86);
+}
+
+* {
+  --vp-c-neutral: var(--vp-c-black);
+  --vp-c-neutral-inverse: var(--vp-c-white);
+  --vp-input-border-color: var(--vp-c-border);
+  --vp-input-switch-bg-color: var(--vp-c-gray-soft);
+  --vp-c-brand-1: var(--vp-c-indigo-1);
+}
+
+.VPSwitch {
+  position: relative;
+  border-radius: 11px;
+  display: block;
+  width: 40px;
+  height: 22px;
+  flex-shrink: 0;
+  border: 1px solid var(--vp-input-border-color);
+  background-color: var(--vp-input-switch-bg-color);
+  transition: border-color 0.25s !important;
+}
+
+.VPSwitch:hover {
+  border-color: var(--vp-c-brand-1);
+}
+
+.check {
+  position: absolute;
+  top: 1px;
+  /*rtl:ignore*/
+  left: 1px;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background-color: var(--vp-c-neutral-inverse);
+  box-shadow: var(--vp-shadow-1);
+  transition: transform 0.25s !important;
+}
+
+.icon {
+  position: relative;
+  display: block;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  overflow: hidden;
+}
+
+.icon svg {
+  position: absolute;
+  top: 3px;
+  left: 3px;
+  width: 12px;
+  height: 12px;
+  fill: var(--vp-c-text-2);
+}
+
+.dark .icon svg {
+  fill: var(--vp-c-text-1);
+  transition: opacity 0.25s !important;
+}
+</style>

+ 0 - 9
frontend/src/dark.less

@@ -1,9 +0,0 @@
-@import "ant-design-vue/dist/antd.dark";
-
-.directive-editor-extra {
-    background-color: rgba(0, 0, 0, 0.84) !important;
-}
-
-.issue-cert-log-container {
-    background-color: rgba(0, 0, 0, 0.84) !important;
-}

+ 4 - 4
frontend/src/gettext.ts

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

+ 27 - 27
frontend/src/language/constants.ts

@@ -3,34 +3,34 @@ import gettext from '@/gettext'
 const {$gettext} = gettext
 
 export const msg = [
-    $gettext('The username or password is incorrect'),
-    $gettext('Prohibit changing root password in demo'),
-    $gettext('Prohibit deleting the default user'),
-    $gettext('Failed to get certificate information'),
+  $gettext('The username or password is incorrect'),
+  $gettext('Prohibit changing root password in demo'),
+  $gettext('Prohibit deleting the default user'),
+  $gettext('Failed to get certificate information'),
 
-    $gettext('Generating private key for registering account'),
-    $gettext('Preparing lego configurations'),
-    $gettext('Creating client facilitates communication with the CA server'),
-    $gettext('Using HTTP01 challenge provider'),
-    $gettext('Using DNS01 challenge provider'),
-    $gettext('Setting environment variables'),
-    $gettext('Cleaning environment variables'),
-    $gettext('Registering user'),
-    $gettext('Obtaining certificate'),
-    $gettext('Writing certificate to disk'),
-    $gettext('Writing certificate private key to disk'),
-    $gettext('Reloading nginx'),
-    $gettext('Finished'),
-    $gettext('Issued certificate successfully'),
+  $gettext('Generating private key for registering account'),
+  $gettext('Preparing lego configurations'),
+  $gettext('Creating client facilitates communication with the CA server'),
+  $gettext('Using HTTP01 challenge provider'),
+  $gettext('Using DNS01 challenge provider'),
+  $gettext('Setting environment variables'),
+  $gettext('Cleaning environment variables'),
+  $gettext('Registering user'),
+  $gettext('Obtaining certificate'),
+  $gettext('Writing certificate to disk'),
+  $gettext('Writing certificate private key to disk'),
+  $gettext('Reloading nginx'),
+  $gettext('Finished'),
+  $gettext('Issued certificate successfully'),
 
-    $gettext('Initialing core upgrader'),
-    $gettext('Initial core upgrader error'),
-    $gettext('Downloading latest release'),
-    $gettext('Download latest release error'),
-    $gettext('Performing core upgrade'),
-    $gettext('Perform core upgrade error'),
-    $gettext('Upgraded successfully'),
+  $gettext('Initialing core upgrader'),
+  $gettext('Initial core upgrader error'),
+  $gettext('Downloading latest release'),
+  $gettext('Download latest release error'),
+  $gettext('Performing core upgrade'),
+  $gettext('Perform core upgrade error'),
+  $gettext('Upgraded successfully'),
 
-    $gettext('File exists'),
-    $gettext('Requested with wrong parameters')
+  $gettext('File exists'),
+  $gettext('Requested with wrong parameters')
 ]

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 170 - 279
frontend/src/language/en/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 178 - 289
frontend/src/language/es/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 178 - 289
frontend/src/language/fr_FR/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 171 - 316
frontend/src/language/messages.pot


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 170 - 279
frontend/src/language/ru_RU/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
frontend/src/language/translations.json


BIN=BIN
frontend/src/language/zh_CN/app.mo


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 178 - 289
frontend/src/language/zh_CN/app.po


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 178 - 289
frontend/src/language/zh_TW/app.po


+ 146 - 141
frontend/src/layouts/BaseLayout.vue

@@ -7,212 +7,217 @@ import zh_CN from 'ant-design-vue/es/locale/zh_CN'
 import zh_TW from 'ant-design-vue/es/locale/zh_TW'
 import en_US from 'ant-design-vue/es/locale/en_US'
 import {computed, ref} from 'vue'
+import {theme} from 'ant-design-vue'
 import _ from 'lodash'
 
 import gettext from '@/gettext'
+import {useSettingsStore} from '@/pinia'
 
 const drawer_visible = ref(false)
 const collapsed = ref(collapse())
 
 addEventListener('resize', _.throttle(() => {
-    collapsed.value = collapse()
+  collapsed.value = collapse()
 }, 50))
 
 function getClientWidth() {
-    return document.body.clientWidth
+  return document.body.clientWidth
 }
 
 function collapse() {
-    return getClientWidth() < 1280
+  return getClientWidth() < 1280
 }
 
 const lang = computed(() => {
-    switch (gettext.current) {
-        case 'zh_CN':
-            return zh_CN
-        case 'zh_TW':
-            return zh_TW
-        default:
-            return en_US
-    }
+  switch (gettext.current) {
+    case 'zh_CN':
+      return zh_CN
+    case 'zh_TW':
+      return zh_TW
+    default:
+      return en_US
+  }
 })
-
+const settings = useSettingsStore()
+const is_theme_dark = computed(() => settings.theme == 'dark')
 </script>
 <template>
-    <a-config-provider :locale="lang" :autoInsertSpaceInButton="false">
-        <a-layout style="min-height: 100%;">
-            <div class="drawer-sidebar">
-                <a-drawer
-                    :closable="false"
-                    v-model:visible="drawer_visible"
-                    placement="left"
-                    @close="drawer_visible=false"
-                    width="256"
-                >
-                    <side-bar/>
-                </a-drawer>
-            </div>
-
-            <a-layout-sider
-                v-model:collapsed="collapsed"
-                :collapsible="true"
-                :style="{zIndex: 11}"
-                theme="light"
-                class="layout-sider"
-            >
-                <side-bar/>
-            </a-layout-sider>
-
-            <a-layout>
-                <a-layout-header :style="{position: 'sticky', top: '0', zIndex: 10, width:'100%'}">
-                    <header-layout @clickUnFold="drawer_visible=true"/>
-                </a-layout-header>
-
-                <a-layout-content>
-                    <page-header/>
-                    <div class="router-view">
-                        <router-view v-slot="{ Component, route }">
-                            <transition name="slide-fade">
-                                <component :is="Component" :key="route.path"/>
-                            </transition>
-                        </router-view>
-                    </div>
-                </a-layout-content>
-
-                <a-layout-footer>
-                    <footer-layout/>
-                </a-layout-footer>
-            </a-layout>
-
-        </a-layout>
-    </a-config-provider>
+  <a-config-provider :theme="{
+      algorithm: is_theme_dark?theme.darkAlgorithm:theme.defaultAlgorithm,
+    }" :locale="lang" :autoInsertSpaceInButton="false">
+    <a-layout style="min-height: 100%;">
+      <div class="drawer-sidebar">
+        <a-drawer
+          :closable="false"
+          v-model:open="drawer_visible"
+          placement="left"
+          @close="drawer_visible=false"
+          width="256"
+        >
+          <side-bar/>
+        </a-drawer>
+      </div>
+
+      <a-layout-sider
+        v-model:collapsed="collapsed"
+        :collapsible="true"
+        :style="{zIndex: 11}"
+        theme="light"
+        class="layout-sider"
+      >
+        <side-bar/>
+      </a-layout-sider>
+
+      <a-layout>
+        <a-layout-header :style="{position: 'sticky', top: '0', zIndex: 10, width:'100%'}">
+          <header-layout @clickUnFold="drawer_visible=true"/>
+        </a-layout-header>
+
+        <a-layout-content>
+          <page-header/>
+          <div class="router-view">
+            <router-view v-slot="{ Component, route }">
+              <transition name="slide-fade">
+                <component :is="Component" :key="route.path"/>
+              </transition>
+            </router-view>
+          </div>
+        </a-layout-content>
+
+        <a-layout-footer>
+          <footer-layout/>
+        </a-layout-footer>
+      </a-layout>
+
+    </a-layout>
+  </a-config-provider>
 </template>
 
 <style lang="less" scoped>
 .layout-sider {
-    @media (max-width: 600px) {
-        display: none;
-    }
+  @media (max-width: 600px) {
+    display: none;
+  }
 }
 
 .drawer-sidebar {
-    @media (min-width: 600px) {
-        display: none;
-    }
+  @media (min-width: 600px) {
+    display: none;
+  }
 }
 </style>
 
 <style lang="less">
 .layout-sider .sidebar {
-    ul.ant-menu-inline.ant-menu-root {
-        height: calc(100vh - 160px);
-        overflow-y: auto;
-        overflow-x: hidden;
-
-        .ant-menu-item {
-            width: unset;
-        }
-    }
+  ul.ant-menu-inline.ant-menu-root {
+    height: calc(100vh - 160px);
+    overflow-y: auto;
+    overflow-x: hidden;
 
-    ul.ant-menu-inline-collapsed {
-        height: calc(100vh - 200px);
-        overflow-y: auto;
-        overflow-x: hidden;
+    .ant-menu-item {
+      width: unset;
     }
+  }
+
+  ul.ant-menu-inline-collapsed {
+    height: calc(100vh - 200px);
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
 }
 </style>
 
 <style lang="less">
 .slide-fade-enter-active {
-    transition: all .3s ease-in-out;
+  transition: all .3s ease-in-out;
 }
 
 .slide-fade-leave-active {
-    transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
+  transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
 }
 
 .slide-fade-enter-from, .slide-fade-enter-to, .slide-fade-leave-to
-    /* .slide-fade-leave-active for below version 2.1.8 */ {
-    transform: translateX(10px);
-    opacity: 0;
+  /* .slide-fade-leave-active for below version 2.1.8 */ {
+  transform: translateX(10px);
+  opacity: 0;
 }
 
 body {
-    overflow: unset !important;
+  overflow: unset !important;
 }
 
 .dark {
-    h1, h2, h3, h4, h5, h6, p {
-        color: #fafafa !important;
-    }
+  h1, h2, h3, h4, h5, h6, p {
+    color: #fafafa !important;
+  }
 
-    .ant-checkbox-indeterminate {
-        .ant-checkbox-inner {
-            background-color: transparent !important;
-        }
+  .ant-checkbox-indeterminate {
+    .ant-checkbox-inner {
+      background-color: transparent !important;
     }
+  }
 
-    .ant-menu {
-        background: unset !important;
-    }
-
-    .ant-layout-header {
-        background-color: #1f1f1f !important;
-    }
+  .ant-menu {
+    background: unset !important;
+  }
 
-    .ant-card {
-        background-color: #1f1f1f !important;
-    }
+  .ant-layout-header {
+    background-color: #1f1f1f !important;
+  }
 
-    .ant-layout-sider {
-        background-color: rgb(20, 20, 20) !important;
+  .ant-card {
+    background-color: #1f1f1f !important;
+  }
 
-        .ant-layout-sider-trigger {
-            background-color: rgb(20, 20, 20) !important;
-        }
+  .ant-layout-sider {
+    background-color: rgb(20, 20, 20) !important;
 
-        .ant-menu {
-            border-right: 0 !important;
-        }
+    .ant-layout-sider-trigger {
+      background-color: rgb(20, 20, 20) !important;
+    }
 
-        &.ant-layout-sider-has-trigger {
-            padding-bottom: 0;
-        }
+    .ant-menu {
+      border-right: 0 !important;
+    }
 
-        box-shadow: 2px 0 8px rgba(29, 35, 41, 0.05);
+    &.ant-layout-sider-has-trigger {
+      padding-bottom: 0;
     }
 
+    box-shadow: 2px 0 8px rgba(29, 35, 41, 0.05);
+  }
+
 }
 
 .ant-layout-header {
-    padding: 0 !important;
-    background-color: #fff !important;
+  padding: 0 !important;
+  background-color: #fff !important;
 }
 
 
 .ant-layout-sider {
-    background-color: #ffffff;
+  background-color: #ffffff;
 
-    &.ant-layout-sider-has-trigger {
-        padding-bottom: 0;
-    }
+  &.ant-layout-sider-has-trigger {
+    padding-bottom: 0;
+  }
 
-    box-shadow: 2px 0 8px rgba(29, 35, 41, 0.05);
+  box-shadow: 2px 0 8px rgba(29, 35, 41, 0.05);
 }
 
 .ant-drawer-body {
-    .sidebar .logo {
-        box-shadow: 0 1px 0 0 #e8e8e8;
-    }
+  .sidebar .logo {
+    box-shadow: 0 1px 0 0 #e8e8e8;
+  }
 
-    .ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
-        border-right: 0 !important;
-    }
+  .ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
+    border-right: 0 !important;
+  }
 }
 
 
 .ant-table-small {
-    font-size: 13px;
+  font-size: 13px;
 }
 
 .ant-card-bordered {
@@ -220,32 +225,32 @@ body {
 }
 
 .header-notice-wrapper .ant-tabs-content {
-    max-height: 250px;
+  max-height: 250px;
 }
 
 .header-notice-wrapper .ant-tabs-tabpane-active {
-    overflow-y: scroll;
+  overflow-y: scroll;
 }
 
 .ant-layout-footer {
-    @media (max-width: 320px) {
-        padding: 10px;
-    }
+  @media (max-width: 320px) {
+    padding: 10px;
+  }
 }
 
 .ant-layout-content {
-    min-height: auto;
-
-    .router-view {
-        padding: 20px;
-        @media (max-width: 512px) {
-            padding: 20px 0;
-        }
-        position: relative;
+  min-height: auto;
+
+  .router-view {
+    padding: 20px;
+    @media (max-width: 512px) {
+      padding: 20px 0;
     }
+    position: relative;
+  }
 }
 
 .ant-layout-footer {
-    text-align: center;
+  text-align: center;
 }
 </style>

+ 2 - 2
frontend/src/layouts/BaseRouterView.vue

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

+ 8 - 8
frontend/src/layouts/FooterLayout.vue

@@ -1,17 +1,17 @@
 <template>
-    <div class="footer center">
-        Copyright © 2020 - {{ thisYear }} Nginx UI
-    </div>
+  <div class="footer center">
+    Copyright © 2020 - {{ thisYear }} Nginx UI
+  </div>
 </template>
 
 <script>
 export default {
-    name: 'FooterComponent',
-    data() {
-        return {
-            thisYear: new Date().getFullYear()
-        }
+  name: 'FooterComponent',
+  data() {
+    return {
+      thisYear: new Date().getFullYear()
     }
+  }
 }
 </script>
 

+ 45 - 42
frontend/src/layouts/HeaderLayout.vue

@@ -6,80 +6,83 @@ import auth from '@/api/auth'
 import {HomeOutlined, LogoutOutlined, MenuUnfoldOutlined} from '@ant-design/icons-vue'
 import {useRouter} from 'vue-router'
 import NginxControl from '@/components/NginxControl/NginxControl.vue'
+import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
 
 const {$gettext} = gettext
 
 const router = useRouter()
 
 function logout() {
-    auth.logout().then(() => {
-        message.success($gettext('Logout successful'))
-    }).then(() => {
-        router.push('/login')
-    })
+  auth.logout().then(() => {
+    message.success($gettext('Logout successful'))
+  }).then(() => {
+    router.push('/login')
+  })
 }
 </script>
 
 <template>
-    <div class="header">
-        <div class="tool">
-            <MenuUnfoldOutlined @click="$emit('clickUnFold')"/>
-        </div>
+  <div class="header">
+    <div class="tool">
+      <MenuUnfoldOutlined @click="$emit('clickUnFold')"/>
+    </div>
 
-        <a-space class="user-wrapper" :size="24">
-            <set-language class="set_lang"/>
+    <a-space class="user-wrapper" :size="24">
+      <SetLanguage class="set_lang"/>
 
-            <a href="/">
-                <HomeOutlined/>
-            </a>
+      <SwitchAppearance/>
 
-            <NginxControl/>
+      <a href="/">
+        <HomeOutlined/>
+      </a>
 
-            <a @click="logout">
-                <LogoutOutlined/>
-            </a>
-        </a-space>
-    </div>
+      <NginxControl/>
+
+      <a @click="logout">
+        <LogoutOutlined/>
+      </a>
+    </a-space>
+  </div>
 </template>
 
 
 <style lang="less" scoped>
 .header {
-    height: 64px;
-    padding: 0 20px 0 0;
-    background: transparent;
-    box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
-    width: 100%;
-
-    a {
-        color: #000000;
-    }
+  height: 64px;
+  padding: 0 20px 0 0;
+  background: transparent;
+  box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
+  width: 100%;
+
+  a {
+    color: #000000;
+  }
 }
 
 .dark {
-    .header {
-        box-shadow: 1px 1px 0 0 #404040;
+  .header {
+    box-shadow: 1px 1px 0 0 #404040;
 
-        a {
-            color: #fafafa;
-        }
+    a {
+      color: #fafafa;
     }
+  }
 }
 
 .tool {
-    position: absolute;
-    left: 20px;
-    @media (min-width: 600px) {
-        display: none;
-    }
+  position: absolute;
+  left: 20px;
+  @media (min-width: 600px) {
+    display: none;
+  }
 }
 
 .user-wrapper {
-    position: absolute;
-    right: 28px;
+  position: absolute;
+  right: 28px;
 }
 
 .set_lang {
-    display: inline;
+  display: inline;
 }
 </style>

+ 20 - 20
frontend/src/layouts/Loading.vue

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

+ 87 - 87
frontend/src/layouts/SideBar.vue

@@ -12,134 +12,134 @@ let openKeys = [openSub()]
 const selectedKey = ref([route.name])
 
 function openSub() {
-    let path = route.path
-    let lastSepIndex = path.lastIndexOf('/')
-    return path.substring(1, lastSepIndex)
+  let path = route.path
+  let lastSepIndex = path.lastIndexOf('/')
+  return path.substring(1, lastSepIndex)
 }
 
 watch(route, () => {
-    selectedKey.value = [route.name]
-    const sub = openSub()
-    const p = openKeys.indexOf(sub)
-    if (p === -1) openKeys.push(sub)
+  selectedKey.value = [route.name]
+  const sub = openSub()
+  const p = openKeys.indexOf(sub)
+  if (p === -1) openKeys.push(sub)
 })
 
 const sidebars = computed(() => {
-    return routes[0]['children']
+  return routes[0]['children']
 })
 
 interface meta {
-    icon: any
-    hiddenInSidebar: boolean
-    hideChildren: boolean
+  icon: any
+  hiddenInSidebar: boolean
+  hideChildren: boolean
 }
 
 interface sidebar {
-    path: string
-    name: Function
-    meta: meta,
-    children: sidebar[]
+  path: string
+  name: Function
+  meta: meta,
+  children: sidebar[]
 }
 
 const visible: ComputedRef<sidebar[]> = computed(() => {
 
-    const res: sidebar[] = [];
-
-    (sidebars.value || []).forEach((s) => {
-        if (s.meta && s.meta.hiddenInSidebar) {
-            return
-        }
-        const t: sidebar = {
-            path: s.path,
-            name: s.name,
-            meta: s.meta as meta,
-            children: []
-        };
-
-        (s.children || []).forEach((c: any) => {
-            if (c.meta && c.meta.hiddenInSidebar) {
-                return
-            }
-            t.children.push((c as sidebar))
-        })
-        res.push(t)
+  const res: sidebar[] = [];
+
+  (sidebars.value || []).forEach((s) => {
+    if (s.meta && s.meta.hiddenInSidebar) {
+      return
+    }
+    const t: sidebar = {
+      path: s.path,
+      name: s.name,
+      meta: s.meta as meta,
+      children: []
+    };
+
+    (s.children || []).forEach((c: any) => {
+      if (c.meta && c.meta.hiddenInSidebar) {
+        return
+      }
+      t.children.push((c as sidebar))
     })
+    res.push(t)
+  })
 
 
-    return res
+  return res
 })
 </script>
 
 <template>
-    <div class="sidebar">
-        <logo/>
-
-        <a-menu
-            :openKeys="openKeys"
-            mode="inline"
-            v-model:openKeys="openKeys"
-            v-model:selectedKeys="selectedKey"
-        >
-            <env-indicator/>
-            
-            <template v-for="sidebar in visible">
-                <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren"
-                             :key="sidebar.name"
-                             @click="$router.push('/'+sidebar.path).catch(() => {})">
-                    <component :is="sidebar.meta.icon"/>
-                    <span>{{ sidebar.name() }}</span>
-                </a-menu-item>
-
-                <a-sub-menu v-else :key="sidebar.path">
-                    <template #title>
-                        <component :is="sidebar.meta.icon"/>
-                        <span>{{ sidebar.name() }}</span>
-                    </template>
-                    <a-menu-item v-for="child in sidebar.children" :key="child.name">
-                        <router-link :to="'/'+sidebar.path+'/'+child.path">
-                            {{ child.name() }}
-                        </router-link>
-                    </a-menu-item>
-                </a-sub-menu>
-            </template>
-        </a-menu>
-    </div>
+  <div class="sidebar">
+    <logo/>
+
+    <a-menu
+      :openKeys="openKeys"
+      mode="inline"
+      v-model:openKeys="openKeys"
+      v-model:selectedKeys="selectedKey"
+    >
+      <env-indicator/>
+
+      <template v-for="sidebar in visible">
+        <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren"
+                     :key="sidebar.name"
+                     @click="$router.push('/'+sidebar.path).catch(() => {})">
+          <component :is="sidebar.meta.icon"/>
+          <span>{{ sidebar.name() }}</span>
+        </a-menu-item>
+
+        <a-sub-menu v-else :key="sidebar.path">
+          <template #title>
+            <component :is="sidebar.meta.icon"/>
+            <span>{{ sidebar.name() }}</span>
+          </template>
+          <a-menu-item v-for="child in sidebar.children" :key="child.name">
+            <router-link :to="'/'+sidebar.path+'/'+child.path">
+              {{ child.name() }}
+            </router-link>
+          </a-menu-item>
+        </a-sub-menu>
+      </template>
+    </a-menu>
+  </div>
 </template>
 
 <style lang="less">
 .sidebar {
-    position: sticky;
-    top: 0;
+  position: sticky;
+  top: 0;
 
-    .logo {
-        display: inline-flex;
-        justify-content: center;
-        align-items: center;
+  .logo {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
 
-        img {
-            margin-left: -18px;
-        }
+    img {
+      margin-left: -18px;
     }
+  }
 }
 
 .ant-layout-sider-collapsed .logo {
-    overflow: hidden;
+  overflow: hidden;
 }
 
 .ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
-    border-right: unset;
+  border-right: unset;
 }
 
 .ant-layout-sider-collapsed {
-    .logo {
-        img {
-            margin-left: 0;
-        }
+  .logo {
+    img {
+      margin-left: 0;
+    }
 
 
-        .text {
-            display: none;
-        }
+    .text {
+      display: none;
     }
+  }
 }
 </style>

+ 57 - 57
frontend/src/lib/helper/index.ts

@@ -2,86 +2,86 @@ import dayjs from 'dayjs'
 import relativeTime from 'dayjs/plugin/relativeTime'
 
 function bytesToSize(bytes: number) {
-    if (bytes === 0) return '0 B'
+  if (bytes === 0) return '0 B'
 
-    const k = 1024
+  const k = 1024
 
-    const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
 
-    const i = Math.floor(Math.log(bytes) / Math.log(k))
-    return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
 }
 
 function downloadCsv(header: any, data: any[], fileName: string) {
-    if (!header || !Array.isArray(header) || !Array.isArray(data) || !header.length) {
-        return
+  if (!header || !Array.isArray(header) || !Array.isArray(data) || !header.length) {
+    return
+  }
+  let csvContent = 'data:text/csv;charset=utf-8,\ufeff'
+  const _header = header.map(h => h.title).join(',')
+  const keys = header.map(item => item.key)
+  csvContent += _header + '\n'
+  data.forEach((item, index) => {
+    let dataString = ''
+    for (let i = 0; i < keys.length; i++) {
+      dataString += item[keys[i]] + ','
     }
-    let csvContent = 'data:text/csv;charset=utf-8,\ufeff'
-    const _header = header.map(h => h.title).join(',')
-    const keys = header.map(item => item.key)
-    csvContent += _header + '\n'
-    data.forEach((item, index) => {
-        let dataString = ''
-        for (let i = 0; i < keys.length; i++) {
-            dataString += item[keys[i]] + ','
-        }
-        csvContent += index < data.length ? dataString.replace(/,$/, '\n') : dataString.replace(/,$/, '')
-    })
-    const a = document.createElement('a')
-    a.href = encodeURI(csvContent)
-    a.download = fileName
-    a.click()
-    window.URL.revokeObjectURL(csvContent)
+    csvContent += index < data.length ? dataString.replace(/,$/, '\n') : dataString.replace(/,$/, '')
+  })
+  const a = document.createElement('a')
+  a.href = encodeURI(csvContent)
+  a.download = fileName
+  a.click()
+  window.URL.revokeObjectURL(csvContent)
 }
 
 const urlJoin = (...args: string[]) =>
-    args
-        .join('/')
-        .replace(/[\/]+/g, '/')
-        .replace(/^(.+):\//, '$1://')
-        .replace(/^file:/, 'file:/')
-        .replace(/\/(\?|&|#[^!])/g, '$1')
-        .replace(/\?/g, '&')
-        .replace('&', '?')
+  args
+    .join('/')
+    .replace(/[\/]+/g, '/')
+    .replace(/^(.+):\//, '$1://')
+    .replace(/^file:/, 'file:/')
+    .replace(/\/(\?|&|#[^!])/g, '$1')
+    .replace(/\?/g, '&')
+    .replace('&', '?')
 
 function createEnum(definition: any) {
-    const strToValueMap: any = {}
-    const numToDescMap: any = {}
-    for (const enumName of Object.keys(definition)) {
-        const [value, desc] = definition[enumName]
-        strToValueMap[enumName] = value
-        numToDescMap[value] = desc
-    }
-    return {
-        ...strToValueMap,
-        getDesc(enumName: any) {
-            return (definition[enumName] && definition[enumName][1]) || ''
-        },
-        getDescFromValue(value: any) {
-            return numToDescMap[value] || ''
-        }
+  const strToValueMap: any = {}
+  const numToDescMap: any = {}
+  for (const enumName of Object.keys(definition)) {
+    const [value, desc] = definition[enumName]
+    strToValueMap[enumName] = value
+    numToDescMap[value] = desc
+  }
+  return {
+    ...strToValueMap,
+    getDesc(enumName: any) {
+      return (definition[enumName] && definition[enumName][1]) || ''
+    },
+    getDescFromValue(value: any) {
+      return numToDescMap[value] || ''
     }
+  }
 }
 
 function fromNow(t: string) {
-    dayjs.extend(relativeTime)
-    return dayjs(t).fromNow()
+  dayjs.extend(relativeTime)
+  return dayjs(t).fromNow()
 }
 
 function formatDate(t: string) {
-    return dayjs(t).format('YYYY.MM.DD')
+  return dayjs(t).format('YYYY.MM.DD')
 }
 
 function formatDateTime(t: string) {
-    return dayjs(t).format('YYYY-MM-DD HH:mm:ss')
+  return dayjs(t).format('YYYY-MM-DD HH:mm:ss')
 }
 
 export {
-    bytesToSize,
-    downloadCsv,
-    urlJoin,
-    createEnum,
-    fromNow,
-    formatDate,
-    formatDateTime
+  bytesToSize,
+  downloadCsv,
+  urlJoin,
+  createEnum,
+  fromNow,
+  formatDate,
+  formatDateTime
 }

+ 49 - 49
frontend/src/lib/http/index.ts

@@ -11,67 +11,67 @@ const settings = useSettingsStore()
 const {token} = storeToRefs(user)
 
 let instance = axios.create({
-    baseURL: import.meta.env.VITE_API_ROOT,
-    timeout: 50000,
-    headers: {'Content-Type': 'application/json'},
-    transformRequest: [function (data, headers) {
-        if (!(headers) || headers['Content-Type'] === 'multipart/form-data;charset=UTF-8') {
-            return data
-        } else {
-            headers['Content-Type'] = 'application/json'
-        }
-        return JSON.stringify(data)
-    }]
+  baseURL: import.meta.env.VITE_API_ROOT,
+  timeout: 50000,
+  headers: {'Content-Type': 'application/json'},
+  transformRequest: [function (data, headers) {
+    if (!(headers) || headers['Content-Type'] === 'multipart/form-data;charset=UTF-8') {
+      return data
+    } else {
+      headers['Content-Type'] = 'application/json'
+    }
+    return JSON.stringify(data)
+  }]
 })
 
 
 instance.interceptors.request.use(
-    config => {
-        NProgress.start()
-        if (token) {
-            (config.headers as any).Authorization = token.value
-        }
-        if (settings.environment.id) {
-            (config.headers as any)['X-Node-ID'] = settings.environment.id
-        }
-        return config
-    },
-    err => {
-        return Promise.reject(err)
+  config => {
+    NProgress.start()
+    if (token) {
+      (config.headers as any).Authorization = token.value
+    }
+    if (settings.environment.id) {
+      (config.headers as any)['X-Node-ID'] = settings.environment.id
     }
+    return config
+  },
+  err => {
+    return Promise.reject(err)
+  }
 )
 
 instance.interceptors.response.use(
-    response => {
-        NProgress.done()
-        return Promise.resolve(response.data)
-    },
-    async error => {
-        NProgress.done()
-        switch (error.response.status) {
-            case 401:
-            case 403:
-                user.logout()
-                await router.push('/login')
-                break
-        }
-        return Promise.reject(error.response.data)
+  response => {
+    NProgress.done()
+    return Promise.resolve(response.data)
+  },
+  async error => {
+    NProgress.done()
+    switch (error.response.status) {
+      case 401:
+      case 403:
+        user.logout()
+        await router.push('/login')
+        break
     }
+    return Promise.reject(error.response.data)
+  }
 )
 
 const http = {
-    get(url: string, config: AxiosRequestConfig = {}) {
-        return instance.get<any, any>(url, config)
-    },
-    post(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
-        return instance.post<any, any>(url, data, config)
-    },
-    put(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
-        return instance.put<any, any>(url, data, config)
-    },
-    delete(url: string, config: AxiosRequestConfig = {}) {
-        return instance.delete<any, any>(url, config)
-    }
+  get(url: string, config: AxiosRequestConfig = {}) {
+    return instance.get<any, any>(url, config)
+  },
+  post(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
+    return instance.post<any, any>(url, data, config)
+  },
+  put(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
+    return instance.put<any, any>(url, data, config)
+  },
+  delete(url: string, config: AxiosRequestConfig = {}) {
+    return instance.delete<any, any>(url, config)
+  }
 }
 
 

+ 0 - 32
frontend/src/lib/theme/index.ts

@@ -1,32 +0,0 @@
-function changeCss(css: string, value: string) {
-    const body = document.body.style
-    body.setProperty(css, value)
-}
-
-function changeTheme(theme: string) {
-    const head = document.head
-    document.getElementById('theme')?.remove()
-    const styleDom = document.createElement('style')
-    styleDom.id = 'theme'
-    styleDom.innerHTML = theme
-    head.appendChild(styleDom)
-}
-
-export const dark_mode = async (enabled: Boolean) => {
-    document.body.setAttribute('class', enabled ? 'dark' : 'light')
-    if (enabled) {
-        changeTheme((await import('@/dark.less?inline')).default)
-        changeCss('--page-bg-color', '#141414')
-        changeCss('--head-bg-color', 'rgba(0, 0, 0, 0.5)')
-        changeCss('--line-color', '#2e2e2e')
-        changeCss('--content-bg-color', 'rgb(255 255 255 / 4%)')
-        changeCss('--text-color', 'rgba(255, 255, 255, 0.85)')
-    } else {
-        changeTheme((await import('@/style.less?inline')).default)
-        changeCss('--page-bg-color', 'white')
-        changeCss('--head-bg-color', 'rgba(255, 255, 255, 0.7)')
-        changeCss('--line-color', '#e8e8e8')
-        changeCss('--content-bg-color', '#f0f2f5')
-        changeCss('--text-color', 'rgba(0, 0, 0, 0.85)')
-    }
-}

+ 11 - 11
frontend/src/lib/websocket/index.ts

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

+ 0 - 1
frontend/src/main.ts

@@ -3,7 +3,6 @@ import {createPinia} from 'pinia'
 import gettext from './gettext'
 import App from './App.vue'
 import router from './routes'
-//import 'ant-design-vue/dist/antd.less'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 import {useSettingsStore} from '@/pinia'
 import {autoAnimatePlugin} from '@formkit/auto-animate/vue'

+ 2 - 2
frontend/src/pinia/index.ts

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

+ 29 - 28
frontend/src/pinia/moudule/settings.ts

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

+ 16 - 16
frontend/src/pinia/moudule/user.ts

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

+ 191 - 191
frontend/src/routes/index.ts

@@ -3,16 +3,16 @@ import gettext from '../gettext'
 import {useUserStore} from '@/pinia'
 
 import {
-    CloudOutlined,
-    CodeOutlined,
-    DatabaseOutlined,
-    FileOutlined,
-    FileTextOutlined,
-    HomeOutlined,
-    InfoCircleOutlined,
-    SafetyCertificateOutlined,
-    SettingOutlined,
-    UserOutlined
+  CloudOutlined,
+  CodeOutlined,
+  DatabaseOutlined,
+  FileOutlined,
+  FileTextOutlined,
+  HomeOutlined,
+  InfoCircleOutlined,
+  SafetyCertificateOutlined,
+  SettingOutlined,
+  UserOutlined
 } from '@ant-design/icons-vue'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
@@ -20,204 +20,204 @@ import 'nprogress/nprogress.css'
 const {$gettext} = gettext
 
 export const routes = [
-    {
-        path: '/',
-        name: () => $gettext('Home'),
-        component: () => import('@/layouts/BaseLayout.vue'),
-        redirect: '/dashboard',
+  {
+    path: '/',
+    name: () => $gettext('Home'),
+    component: () => import('@/layouts/BaseLayout.vue'),
+    redirect: '/dashboard',
+    children: [
+      {
+        path: 'dashboard',
+        component: () => import('@/views/dashboard/DashBoard.vue'),
+        name: () => $gettext('Dashboard'),
+        meta: {
+          // hiddenHeaderContent: true,
+          icon: HomeOutlined
+        }
+      },
+      {
+        path: 'domain',
+        name: () => $gettext('Manage Sites'),
+        component: () => import('@/layouts/BaseRouterView.vue'),
+        meta: {
+          icon: CloudOutlined
+        },
+        redirect: '/domain/list',
+        children: [{
+          path: 'list',
+          name: () => $gettext('Sites List'),
+          component: () => import('@/views/domain/DomainList.vue')
+        }, {
+          path: 'add',
+          name: () => $gettext('Add Site'),
+          component: () => import('@/views/domain/DomainAdd.vue')
+        }, {
+          path: ':name',
+          name: () => $gettext('Edit Site'),
+          component: () => import('@/views/domain/DomainEdit.vue'),
+          meta: {
+            hiddenInSidebar: true
+          }
+        }]
+      },
+      {
+        path: 'config',
+        name: () => $gettext('Manage Configs'),
+        component: () => import('@/views/config/Config.vue'),
+        meta: {
+          icon: FileOutlined,
+          hideChildren: true
+        }
+      },
+      {
+        path: 'config/:name+/edit',
+        name: () => $gettext('Edit Configuration'),
+        component: () => import('@/views/config/ConfigEdit.vue'),
+        meta: {
+          hiddenInSidebar: true
+        }
+      },
+      {
+        path: 'cert',
+        name: () => $gettext('Certification'),
+        component: () => import('@/layouts/BaseRouterView.vue'),
+        meta: {
+          icon: SafetyCertificateOutlined
+        },
         children: [
-            {
-                path: 'dashboard',
-                component: () => import('@/views/dashboard/DashBoard.vue'),
-                name: () => $gettext('Dashboard'),
-                meta: {
-                    // hiddenHeaderContent: true,
-                    icon: HomeOutlined
-                }
-            },
-            {
-                path: 'domain',
-                name: () => $gettext('Manage Sites'),
-                component: () => import('@/layouts/BaseRouterView.vue'),
-                meta: {
-                    icon: CloudOutlined
-                },
-                redirect: '/domain/list',
-                children: [{
-                    path: 'list',
-                    name: () => $gettext('Sites List'),
-                    component: () => import('@/views/domain/DomainList.vue')
-                }, {
-                    path: 'add',
-                    name: () => $gettext('Add Site'),
-                    component: () => import('@/views/domain/DomainAdd.vue')
-                }, {
-                    path: ':name',
-                    name: () => $gettext('Edit Site'),
-                    component: () => import('@/views/domain/DomainEdit.vue'),
-                    meta: {
-                        hiddenInSidebar: true
-                    }
-                }]
-            },
-            {
-                path: 'config',
-                name: () => $gettext('Manage Configs'),
-                component: () => import('@/views/config/Config.vue'),
-                meta: {
-                    icon: FileOutlined,
-                    hideChildren: true
-                }
-            },
-            {
-                path: 'config/:name+/edit',
-                name: () => $gettext('Edit Configuration'),
-                component: () => import('@/views/config/ConfigEdit.vue'),
-                meta: {
-                    hiddenInSidebar: true
-                }
-            },
-            {
-                path: 'cert',
-                name: () => $gettext('Certification'),
-                component: () => import('@/layouts/BaseRouterView.vue'),
-                meta: {
-                    icon: SafetyCertificateOutlined
-                },
-                children: [
-                    {
-                        path: 'list',
-                        name: () => $gettext('Certification List'),
-                        component: () => import('@/views/cert/Cert.vue')
-                    },
-                    {
-                        path: 'dns_credential',
-                        name: () => $gettext('DNS Credentials'),
-                        component: () => import('@/views/cert/DNSCredential.vue')
-                    }
-                ]
-            },
-            {
-                path: 'terminal',
-                name: () => $gettext('Terminal'),
-                component: () => import('@/views/pty/Terminal.vue'),
-                meta: {
-                    icon: CodeOutlined
-                }
-            },
-            {
-                path: 'nginx_log',
-                name: () => $gettext('Nginx Log'),
-                meta: {
-                    icon: FileTextOutlined
-                },
-                children: [{
-                    path: 'access',
-                    name: () => $gettext('Access Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue')
-                }, {
-                    path: 'error',
-                    name: () => $gettext('Error Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue')
-                }, {
-                    path: 'site',
-                    name: () => $gettext('Site Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue'),
-                    meta: {
-                        hiddenInSidebar: true
-                    }
-                }]
-            },
-            {
-                path: 'environment',
-                name: () => $gettext('Environment'),
-                component: () => import('@/views/environment/Environment.vue'),
-                meta: {
-                    icon: DatabaseOutlined
-                }
-            },
-            {
-                path: 'user',
-                name: () => $gettext('Manage Users'),
-                component: () => import('@/views/user/User.vue'),
-                meta: {
-                    icon: UserOutlined
-                }
-            },
-            {
-                path: 'preference',
-                name: () => $gettext('Preference'),
-                component: () => import('@/views/preference/Preference.vue'),
-                meta: {
-                    icon: SettingOutlined
-                }
-            },
-            {
-                path: 'system',
-                name: () => $gettext('System'),
-                redirect: 'system/about',
-                meta: {
-                    icon: InfoCircleOutlined
-                },
-                children: [{
-                    path: 'about',
-                    name: () => $gettext('About'),
-                    component: () => import('@/views/system/About.vue')
-                }, {
-                    path: 'upgrade',
-                    name: () => $gettext('Upgrade'),
-                    component: () => import('@/views/system/Upgrade.vue')
-                }]
-            }
+          {
+            path: 'list',
+            name: () => $gettext('Certification List'),
+            component: () => import('@/views/cert/Cert.vue')
+          },
+          {
+            path: 'dns_credential',
+            name: () => $gettext('DNS Credentials'),
+            component: () => import('@/views/cert/DNSCredential.vue')
+          }
         ]
-    },
-    {
-        path: '/install',
-        name: () => $gettext('Install'),
-        component: () => import('@/views/other/Install.vue'),
-        meta: {noAuth: true}
-    },
-    {
-        path: '/login',
-        name: () => $gettext('Login'),
-        component: () => import('@/views/other/Login.vue'),
-        meta: {noAuth: true}
-    },
-    {
-        path: '/:pathMatch(.*)*',
-        name: () => $gettext('Not Found'),
-        component: () => import('@/views/other/Error.vue'),
-        meta: {noAuth: true, status_code: 404, error: () => $gettext('Not Found')}
-    }
+      },
+      {
+        path: 'terminal',
+        name: () => $gettext('Terminal'),
+        component: () => import('@/views/pty/Terminal.vue'),
+        meta: {
+          icon: CodeOutlined
+        }
+      },
+      {
+        path: 'nginx_log',
+        name: () => $gettext('Nginx Log'),
+        meta: {
+          icon: FileTextOutlined
+        },
+        children: [{
+          path: 'access',
+          name: () => $gettext('Access Logs'),
+          component: () => import('@/views/nginx_log/NginxLog.vue')
+        }, {
+          path: 'error',
+          name: () => $gettext('Error Logs'),
+          component: () => import('@/views/nginx_log/NginxLog.vue')
+        }, {
+          path: 'site',
+          name: () => $gettext('Site Logs'),
+          component: () => import('@/views/nginx_log/NginxLog.vue'),
+          meta: {
+            hiddenInSidebar: true
+          }
+        }]
+      },
+      {
+        path: 'environment',
+        name: () => $gettext('Environment'),
+        component: () => import('@/views/environment/Environment.vue'),
+        meta: {
+          icon: DatabaseOutlined
+        }
+      },
+      {
+        path: 'user',
+        name: () => $gettext('Manage Users'),
+        component: () => import('@/views/user/User.vue'),
+        meta: {
+          icon: UserOutlined
+        }
+      },
+      {
+        path: 'preference',
+        name: () => $gettext('Preference'),
+        component: () => import('@/views/preference/Preference.vue'),
+        meta: {
+          icon: SettingOutlined
+        }
+      },
+      {
+        path: 'system',
+        name: () => $gettext('System'),
+        redirect: 'system/about',
+        meta: {
+          icon: InfoCircleOutlined
+        },
+        children: [{
+          path: 'about',
+          name: () => $gettext('About'),
+          component: () => import('@/views/system/About.vue')
+        }, {
+          path: 'upgrade',
+          name: () => $gettext('Upgrade'),
+          component: () => import('@/views/system/Upgrade.vue')
+        }]
+      }
+    ]
+  },
+  {
+    path: '/install',
+    name: () => $gettext('Install'),
+    component: () => import('@/views/other/Install.vue'),
+    meta: {noAuth: true}
+  },
+  {
+    path: '/login',
+    name: () => $gettext('Login'),
+    component: () => import('@/views/other/Login.vue'),
+    meta: {noAuth: true}
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    name: () => $gettext('Not Found'),
+    component: () => import('@/views/other/Error.vue'),
+    meta: {noAuth: true, status_code: 404, error: () => $gettext('Not Found')}
+  }
 ]
 
 const router = createRouter({
-    history: createWebHashHistory(),
-    // @ts-ignore
-    routes: routes
+  history: createWebHashHistory(),
+  // @ts-ignore
+  routes: routes
 })
 
 NProgress.configure({showSpinner: false})
 
 router.beforeEach((to, from, next) => {
-    // @ts-ignore
-    document.title = to.name?.() + ' | Nginx UI'
+  // @ts-ignore
+  document.title = to.name?.() + ' | Nginx UI'
 
-    NProgress.start()
+  NProgress.start()
 
-    const user = useUserStore()
-    const {is_login} = user
+  const user = useUserStore()
+  const {is_login} = user
 
-    if (to.meta.noAuth || is_login) {
-        next()
-    } else {
-        next({path: '/login', query: {next: to.fullPath}})
-    }
+  if (to.meta.noAuth || is_login) {
+    next()
+  } else {
+    next({path: '/login', query: {next: to.fullPath}})
+  }
 
 })
 
 router.afterEach(() => {
-    NProgress.done()
+  NProgress.done()
 })
 
 export default router

+ 0 - 1
frontend/src/style.less

@@ -1 +0,0 @@
-@import 'ant-design-vue/dist/antd.less';

+ 1 - 1
frontend/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.4","build_id":19,"total_build":223}
+{"version":"2.0.0-beta.4","build_id":25,"total_build":229}

+ 102 - 102
frontend/src/views/cert/Cert.vue

@@ -13,118 +13,118 @@ import {h} from 'vue'
 const {$gettext, interpolate} = useGettext()
 
 const columns = [{
-    title: () => $gettext('Name'),
-    dataIndex: 'name',
-    sorter: true,
-    pithy: true,
-    customRender: (args: customRender) => {
-        const {text, record} = args
-        if (!text) {
-            return h('div', record.domain)
-        }
-        return h('div', text)
-    },
-    edit: {
-        type: input
-    },
-    search: true
+  title: () => $gettext('Name'),
+  dataIndex: 'name',
+  sorter: true,
+  pithy: true,
+  customRender: (args: customRender) => {
+    const {text, record} = args
+    if (!text) {
+      return h('div', record.domain)
+    }
+    return h('div', text)
+  },
+  edit: {
+    type: input
+  },
+  search: true
 }, {
-    title: () => $gettext('Config Name'),
-    dataIndex: 'filename',
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Config Name'),
+  dataIndex: 'filename',
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Auto Cert'),
-    dataIndex: 'auto_cert',
-    customRender: (args: customRender) => {
-        const template: any = []
-        const {text, column} = args
-        if (text === true || text > 0) {
-            template.push(<Badge status="success"/>)
-            template.push($gettext('Enabled'))
-        } else {
-            template.push(<Badge status="warning"/>)
-            template.push($gettext('Disabled'))
-        }
-        return h('div', template)
-    },
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Auto Cert'),
+  dataIndex: 'auto_cert',
+  customRender: (args: customRender) => {
+    const template: any = []
+    const {text, column} = args
+    if (text === true || text > 0) {
+      template.push(<Badge status="success"/>)
+      template.push($gettext('Enabled'))
+    } else {
+      template.push(<Badge status="warning"/>)
+      template.push($gettext('Disabled'))
+    }
+    return h('div', template)
+  },
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('SSL Certificate Path'),
-    dataIndex: 'ssl_certificate_path',
-    edit: {
-        type: input
-    },
-    display: false
+  title: () => $gettext('SSL Certificate Path'),
+  dataIndex: 'ssl_certificate_path',
+  edit: {
+    type: input
+  },
+  display: false
 }, {
-    title: () => $gettext('SSL Certificate Key Path'),
-    dataIndex: 'ssl_certificate_key_path',
-    edit: {
-        type: input
-    },
-    display: false
+  title: () => $gettext('SSL Certificate Key Path'),
+  dataIndex: 'ssl_certificate_key_path',
+  edit: {
+    type: input
+  },
+  display: false
 }, {
-    title: () => $gettext('Updated at'),
-    dataIndex: 'updated_at',
-    customRender: datetime,
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Updated at'),
+  dataIndex: 'updated_at',
+  customRender: datetime,
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Action'),
-    dataIndex: 'action'
+  title: () => $gettext('Action'),
+  dataIndex: 'action'
 }]
 </script>
 
 <template>
-    <std-curd :title="$gettext('Certification')" :api="cert" :columns="columns"
-              row-key="name"
-    >
-        <template #beforeEdit="{data}">
-            <template v-if="data.auto_cert===1">
-                <div style="margin-bottom: 15px">
-                    <a-alert
-                        :message="$gettext('Auto cert is enabled, please do not modify this certification.')"
-                        type="info"
-                        show-icon/>
-                </div>
-                <div v-if="!data.filename" style="margin-bottom: 15px">
-                    <a-alert
-                        :message="$gettext('This auto-cert item is invalid, please remove it.')"
-                        type="error"
-                        show-icon/>
-                </div>
-                <div v-else-if="!data.domains" style="margin-bottom: 15px">
-                    <a-alert
-                        :message="interpolate($gettext('Domains list is empty, try to reopen auto-cert for %{config}'), {config: data.filename})"
-                        type="error"
-                        show-icon/>
-                </div>
-                <div v-if="data.log" style="margin-bottom: 15px">
-                    <a-form layout="vertical">
-                        <a-form-item :label="$gettext('Auto-Cert Log')">
-                            <p>{{ data.log }}</p>
-                        </a-form-item>
-                    </a-form>
-                </div>
-            </template>
-            <a-form layout="vertical" v-if="data.certificate_info">
-                <a-form-item :label="$gettext('Certificate Status')">
-                    <cert-info :cert="data.certificate_info"/>
-                </a-form-item>
-            </a-form>
-        </template>
-        <template #edit="{data}">
-            <a-form layout="vertical">
-                <a-form-item :label="$gettext('SSL Certification Content')">
-                    <code-editor v-model:content="data.ssl_certification" default-height="200px"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('SSL Certification Key Content')">
-                    <code-editor v-model:content="data.ssl_certification_key" default-height="200px"/>
-                </a-form-item>
-            </a-form>
-        </template>
-    </std-curd>
+  <std-curd :title="$gettext('Certification')" :api="cert" :columns="columns"
+            row-key="name"
+  >
+    <template #beforeEdit="{data}">
+      <template v-if="data.auto_cert===1">
+        <div style="margin-bottom: 15px">
+          <a-alert
+            :message="$gettext('Auto cert is enabled, please do not modify this certification.')"
+            type="info"
+            show-icon/>
+        </div>
+        <div v-if="!data.filename" style="margin-bottom: 15px">
+          <a-alert
+            :message="$gettext('This auto-cert item is invalid, please remove it.')"
+            type="error"
+            show-icon/>
+        </div>
+        <div v-else-if="!data.domains" style="margin-bottom: 15px">
+          <a-alert
+            :message="interpolate($gettext('Domains list is empty, try to reopen auto-cert for %{config}'), {config: data.filename})"
+            type="error"
+            show-icon/>
+        </div>
+        <div v-if="data.log" style="margin-bottom: 15px">
+          <a-form layout="vertical">
+            <a-form-item :label="$gettext('Auto-Cert Log')">
+              <p>{{ data.log }}</p>
+            </a-form-item>
+          </a-form>
+        </div>
+      </template>
+      <a-form layout="vertical" v-if="data.certificate_info">
+        <a-form-item :label="$gettext('Certificate Status')">
+          <cert-info :cert="data.certificate_info"/>
+        </a-form-item>
+      </a-form>
+    </template>
+    <template #edit="{data}">
+      <a-form layout="vertical">
+        <a-form-item :label="$gettext('SSL Certification Content')">
+          <code-editor v-model:content="data.ssl_certification" default-height="200px"/>
+        </a-form-item>
+        <a-form-item :label="$gettext('SSL Certification Key Content')">
+          <code-editor v-model:content="data.ssl_certification_key" default-height="200px"/>
+        </a-form-item>
+      </a-form>
+    </template>
+  </std-curd>
 </template>
 
 <style lang="less" scoped>

+ 43 - 43
frontend/src/views/cert/DNSChallenge.vue

@@ -10,81 +10,81 @@ const providers: any = ref([])
 const data: any = inject('data')!
 
 const code = computed(() => {
-    return data.code
+  return data.code
 })
 
 function init() {
-    data.configuration = {
-        credentials: {},
-        additional: {}
+  data.configuration = {
+    credentials: {},
+    additional: {}
+  }
+  providers.value?.forEach((v: any, k: number) => {
+    if (v.code === code.value) {
+      provider_idx.value = k
     }
-    providers.value?.forEach((v: any, k: number) => {
-        if (v.code === code.value) {
-            provider_idx.value = k
-        }
-    })
+  })
 }
 
 auto_cert.get_dns_providers().then(r => {
-    providers.value = r
+  providers.value = r
 }).then(() => {
-    init()
+  init()
 })
 
 const provider_idx = ref()
 
 const current: any = computed(() => {
-    return providers.value?.[provider_idx.value]
+  return providers.value?.[provider_idx.value]
 })
 
 
 watch(code, init)
 
 watch(current, () => {
-    data.code = current.value.code
-    data.provider = current.value.name
-    auto_cert.get_dns_provider(current.value.code).then(r => {
-        Object.assign(current.value, r)
-    })
+  data.code = current.value.code
+  data.provider = current.value.name
+  auto_cert.get_dns_provider(current.value.code).then(r => {
+    Object.assign(current.value, r)
+  })
 })
 
 const options = computed<SelectProps['options']>(() => {
-    let list: SelectProps['options'] = []
+  let list: SelectProps['options'] = []
 
-    providers.value.forEach((v: any, k: number) => {
-        list!.push({
-            value: k,
-            label: v.name
-        })
+  providers.value.forEach((v: any, k: number) => {
+    list!.push({
+      value: k,
+      label: v.name
     })
+  })
 
-    return list
+  return list
 })
 
 const filterOption = (input: string, option: any) => {
-    return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+  return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
 }
 </script>
 
 <template>
-    <a-form layout="vertical">
-        <a-form-item :label="$gettext('DNS Provider')">
-            <a-select v-model:value="provider_idx" show-search :options="options" :filter-option="filterOption"/>
-        </a-form-item>
-        <template v-if="current?.configuration?.credentials">
-            <h4>{{ $gettext('Credentials') }}</h4>
-            <a-form-item :label="k" v-for="(v,k) in current?.configuration?.credentials"
-                         :extra="v" :rules="[{ required: true }]">
-                <a-input v-model:value="data.configuration.credentials[k]"/>
-            </a-form-item>
-        </template>
-        <template v-if="current?.configuration?.additional">
-            <h4>{{ $gettext('Additional') }}</h4>
-            <a-form-item :label="k" v-for="(v,k) in current?.configuration?.additional" :extra="v">
-                <a-input v-model:value="data.configuration.additional[k]"/>
-            </a-form-item>
-        </template>
-    </a-form>
+  <a-form layout="vertical">
+    <a-form-item :label="$gettext('DNS Provider')">
+      <a-select v-model:value="provider_idx" show-search :options="options" :filter-option="filterOption"/>
+    </a-form-item>
+    <template v-if="current?.configuration?.credentials">
+      <h4>{{ $gettext('Credentials') }}</h4>
+      <a-form-item :label="k" v-for="(v,k) in current?.configuration?.credentials"
+                   :extra="v" :rules="[{ required: true }]">
+        <a-input v-model:value="data.configuration.credentials[k]"/>
+      </a-form-item>
+    </template>
+    <template v-if="current?.configuration?.additional">
+      <h4>{{ $gettext('Additional') }}</h4>
+      <a-form-item :label="k" v-for="(v,k) in current?.configuration?.additional" :extra="v">
+        <a-input v-model:value="data.configuration.additional[k]"/>
+      </a-form-item>
+    </template>
+  </a-form>
 </template>
 
 <style lang="less" scoped>

+ 37 - 37
frontend/src/views/cert/DNSCredential.vue

@@ -9,51 +9,51 @@ import {input} from '@/components/StdDataEntry'
 const {$gettext, interpolate} = useGettext()
 
 const columns = [{
-    title: () => $gettext('Name'),
-    dataIndex: 'name',
-    sorter: true,
-    pithy: true,
-    edit: {
-        type: input
-    }
+  title: () => $gettext('Name'),
+  dataIndex: 'name',
+  sorter: true,
+  pithy: true,
+  edit: {
+    type: input
+  }
 }, {
-    title: () => $gettext('Provider'),
-    dataIndex: ['config', 'name'],
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Provider'),
+  dataIndex: ['config', 'name'],
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Updated at'),
-    dataIndex: 'updated_at',
-    customRender: datetime,
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Updated at'),
+  dataIndex: 'updated_at',
+  customRender: datetime,
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Action'),
-    dataIndex: 'action'
+  title: () => $gettext('Action'),
+  dataIndex: 'action'
 }]
 </script>
 
 <template>
-    <std-curd :title="$gettext('DNS Credentials')" :api="dns_credential" :columns="columns"
-              row-key="name"
-    >
-        <template #beforeEdit>
-            <a-alert type="info" show-icon :message="$gettext('Note')">
-                <template #description>
-                    <p v-translate>
-                        Please fill in the API authentication credentials provided by your DNS provider.
-                        We will add one or more TXT records to the DNS records of your domain for ownership
-                        verification.
-                        Once the verification is complete, the records will be removed.
-                        Please note that the time configurations below are all in seconds.
-                    </p>
-                </template>
-            </a-alert>
+  <std-curd :title="$gettext('DNS Credentials')" :api="dns_credential" :columns="columns"
+            row-key="name"
+  >
+    <template #beforeEdit>
+      <a-alert type="info" show-icon :message="$gettext('Note')">
+        <template #description>
+          <p v-translate>
+            Please fill in the API authentication credentials provided by your DNS provider.
+            We will add one or more TXT records to the DNS records of your domain for ownership
+            verification.
+            Once the verification is complete, the records will be removed.
+            Please note that the time configurations below are all in seconds.
+          </p>
         </template>
-        <template #edit="{data}">
-            <d-n-s-challenge/>
-        </template>
-    </std-curd>
+      </a-alert>
+    </template>
+    <template #edit="{data}">
+      <d-n-s-challenge/>
+    </template>
+  </std-curd>
 </template>
 
 <style lang="less" scoped>

+ 25 - 25
frontend/src/views/config/Config.vue

@@ -17,43 +17,43 @@ const table = ref(null)
 const route = useRoute()
 
 const basePath = computed(() => {
-    let dir = route?.query?.dir ?? ''
-    if (dir) dir += '/'
-    return dir
+  let dir = route?.query?.dir ?? ''
+  if (dir) dir += '/'
+  return dir
 })
 
 const get_params = computed(() => {
-    return {
-        dir: basePath.value
-    }
+  return {
+    dir: basePath.value
+  }
 })
 
 const update = ref(1)
 
 watch(get_params, () => {
-    update.value++
+  update.value++
 })
 
 const inspect_config = ref()
 
 watch(route, () => {
-    inspect_config.value?.test()
+  inspect_config.value?.test()
 })
 </script>
 
 <template>
-    <a-card :title="$gettext('Configurations')">
-        <inspect-config ref="inspect_config"/>
-        <std-table
-            :key="update"
-            ref="table"
-            :api="api"
-            :columns="configColumns"
-            :deletable="false"
-            :disable_search="true"
-            row-key="name"
-            :get_params="get_params"
-            @clickEdit="(r, row) => {
+  <a-card :title="$gettext('Configurations')">
+    <inspect-config ref="inspect_config"/>
+    <std-table
+      :key="update"
+      ref="table"
+      :api="api"
+      :columns="configColumns"
+      :deletable="false"
+      :disable_search="true"
+      row-key="name"
+      :get_params="get_params"
+      @clickEdit="(r, row) => {
                 if (!row.is_dir) {
                     $router.push({
                         path: '/config/' + basePath + r + '/edit'
@@ -66,11 +66,11 @@ watch(route, () => {
                     })
                 }
             }"
-        />
-        <footer-tool-bar v-if="basePath">
-            <a-button @click="router.go(-1)">{{ $gettext('Back') }}</a-button>
-        </footer-tool-bar>
-    </a-card>
+    />
+    <footer-tool-bar v-if="basePath">
+      <a-button @click="router.go(-1)">{{ $gettext('Back') }}</a-button>
+    </footer-tool-bar>
+  </a-card>
 </template>
 
 <style scoped>

+ 83 - 83
frontend/src/views/config/ConfigEdit.vue

@@ -17,11 +17,11 @@ const route = useRoute()
 const inspect_config = ref()
 
 const name = computed(() => {
-    const n = route.params.name
-    if (typeof n === 'string') {
-        return n
-    }
-    return n?.join('/')
+  const n = route.params.name
+  if (typeof n === 'string') {
+    return n
+  }
+  return n?.join('/')
 })
 
 const configText = ref('')
@@ -31,108 +31,108 @@ const active_key = ref(['1', '2'])
 const modified_at = ref('')
 
 function init() {
-    if (name.value) {
-        config.get(name.value).then(r => {
-            configText.value = r.config
-            history_chatgpt_record.value = r.chatgpt_messages
-            file_path.value = r.file_path
-            modified_at.value = r.modified_at
-        }).catch(r => {
-            message.error(r.message ?? $gettext('Server error'))
-        })
-    } else {
-        configText.value = ''
-        history_chatgpt_record.value = []
-        file_path.value = ''
-    }
+  if (name.value) {
+    config.get(name.value).then(r => {
+      configText.value = r.config
+      history_chatgpt_record.value = r.chatgpt_messages
+      file_path.value = r.file_path
+      modified_at.value = r.modified_at
+    }).catch(r => {
+      message.error(r.message ?? $gettext('Server error'))
+    })
+  } else {
+    configText.value = ''
+    history_chatgpt_record.value = []
+    file_path.value = ''
+  }
 }
 
 init()
 
 function save() {
-    config.save(name.value, {content: configText.value}).then(r => {
-        configText.value = r.config
-        message.success($gettext('Saved successfully'))
-    }).catch(r => {
-        message.error(interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}))
-    }).finally(() => {
-        inspect_config.value.test()
-    })
+  config.save(name.value, {content: configText.value}).then(r => {
+    configText.value = r.config
+    message.success($gettext('Saved successfully'))
+  }).catch(r => {
+    message.error(interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}))
+  }).finally(() => {
+    inspect_config.value.test()
+  })
 }
 
 function format_code() {
-    ngx.format_code(configText.value).then(r => {
-        configText.value = r.content
-        message.success($gettext('Format successfully'))
-    }).catch(r => {
-        message.error(interpolate($gettext('Format error %{msg}'), {msg: r.message ?? ''}))
-    })
+  ngx.format_code(configText.value).then(r => {
+    configText.value = r.content
+    message.success($gettext('Format successfully'))
+  }).catch(r => {
+    message.error(interpolate($gettext('Format error %{msg}'), {msg: r.message ?? ''}))
+  })
 }
 
 </script>
 
 
 <template>
-    <a-row :gutter="16">
-        <a-col :xs="24" :sm="24" :md="18">
-            <a-card :title="$gettext('Edit Configuration')">
-                <inspect-config ref="inspect_config"/>
-                <code-editor v-model:content="configText"/>
-                <footer-tool-bar>
-                    <a-space>
-                        <a-button @click="$router.go(-1)">
-                            <translate>Back</translate>
-                        </a-button>
-                        <a-button @click="format_code">
-                            <translate>Format Code</translate>
-                        </a-button>
-                        <a-button type="primary" @click="save">
-                            <translate>Save</translate>
-                        </a-button>
-                    </a-space>
-                </footer-tool-bar>
-            </a-card>
-        </a-col>
-
-        <a-col :xs="24" :sm="24" :md="6">
-            <a-card class="col-right">
-                <a-collapse v-model:activeKey="active_key" ghost>
-                    <a-collapse-panel key="1" :header="$gettext('Basic')">
-                        <a-form layout="vertical">
-                            <a-form-item :label="$gettext('Path')">
-                                {{ file_path }}
-                            </a-form-item>
-                            <a-form-item :label="$gettext('Updated at')">
-                                {{ formatDateTime(modified_at) }}
-                            </a-form-item>
-                        </a-form>
-                    </a-collapse-panel>
-                    <a-collapse-panel key="2" header="ChatGPT">
-                        <chat-g-p-t :content="configText" :path="file_path"
-                                    v-model:history_messages="history_chatgpt_record"/>
-                    </a-collapse-panel>
-                </a-collapse>
-            </a-card>
-        </a-col>
-    </a-row>
+  <a-row :gutter="16">
+    <a-col :xs="24" :sm="24" :md="18">
+      <a-card :title="$gettext('Edit Configuration')">
+        <inspect-config ref="inspect_config"/>
+        <code-editor v-model:content="configText"/>
+        <footer-tool-bar>
+          <a-space>
+            <a-button @click="$router.go(-1)">
+              <translate>Back</translate>
+            </a-button>
+            <a-button @click="format_code">
+              <translate>Format Code</translate>
+            </a-button>
+            <a-button type="primary" @click="save">
+              <translate>Save</translate>
+            </a-button>
+          </a-space>
+        </footer-tool-bar>
+      </a-card>
+    </a-col>
+
+    <a-col :xs="24" :sm="24" :md="6">
+      <a-card class="col-right">
+        <a-collapse v-model:activeKey="active_key" ghost>
+          <a-collapse-panel key="1" :header="$gettext('Basic')">
+            <a-form layout="vertical">
+              <a-form-item :label="$gettext('Path')">
+                {{ file_path }}
+              </a-form-item>
+              <a-form-item :label="$gettext('Updated at')">
+                {{ formatDateTime(modified_at) }}
+              </a-form-item>
+            </a-form>
+          </a-collapse-panel>
+          <a-collapse-panel key="2" header="ChatGPT">
+            <chat-g-p-t :content="configText" :path="file_path"
+                        v-model:history_messages="history_chatgpt_record"/>
+          </a-collapse-panel>
+        </a-collapse>
+      </a-card>
+    </a-col>
+  </a-row>
 </template>
 
 <style lang="less" scoped>
 .col-right {
-    position: sticky;
-    top: 78px;
+  position: sticky;
+  top: 78px;
 
-    :deep(.ant-card-body) {
-        max-height: 100vh;
-        overflow-y: scroll;
-    }
+  :deep(.ant-card-body) {
+    max-height: 100vh;
+    overflow-y: scroll;
+  }
 }
 
 :deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
-    padding: 0;
+  padding: 0;
 }
 
 :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
-    padding: 0 0 10px 0;
+  padding: 0 0 10px 0;
 }
 </style>

+ 32 - 32
frontend/src/views/config/InspectConfig.vue

@@ -8,57 +8,57 @@ import logLevel from '@/views/config/constants'
 const {$gettext} = useGettext()
 
 const data = ref({
-    level: 0,
-    message: ''
+  level: 0,
+  message: ''
 })
 
 test()
 
 function test() {
-    ngx.test().then(r => {
-        data.value = r
-    })
+  ngx.test().then(r => {
+    data.value = r
+  })
 }
 
 defineExpose({
-    test
+  test
 })
 </script>
 
 <template>
-    <div class="inspect-container">
-        <a-alert :message="$gettext('Configuration file is test successful')" type="success"
-                 show-icon v-if="data?.level<logLevel.Debug"/>
-        <a-alert
-            :message="$gettext('Warning')"
-            type="warning"
-            show-icon
-            v-else-if="data?.level===logLevel.Warn"
-        >
-            <template #description>
-                {{ data.message }}
-            </template>
-        </a-alert>
+  <div class="inspect-container">
+    <a-alert :message="$gettext('Configuration file is test successful')" type="success"
+             show-icon v-if="data?.level<logLevel.Debug"/>
+    <a-alert
+      :message="$gettext('Warning')"
+      type="warning"
+      show-icon
+      v-else-if="data?.level===logLevel.Warn"
+    >
+      <template #description>
+        {{ data.message }}
+      </template>
+    </a-alert>
 
-        <a-alert
-            :message="$gettext('Error')"
-            type="error"
-            show-icon
-            v-else-if="data?.level>logLevel.Warn"
-        >
-            <template #description>
-                {{ data.message }}
-            </template>
-        </a-alert>
-    </div>
+    <a-alert
+      :message="$gettext('Error')"
+      type="error"
+      show-icon
+      v-else-if="data?.level>logLevel.Warn"
+    >
+      <template #description>
+        {{ data.message }}
+      </template>
+    </a-alert>
+  </div>
 </template>
 
 <style lang="less" scoped>
 .inspect-container {
-    margin-bottom: 20px;
+  margin-bottom: 20px;
 }
 
 :deep(.ant-alert-description) {
-    white-space: pre-line;
+  white-space: pre-line;
 }
 </style>

+ 26 - 26
frontend/src/views/config/config.ts

@@ -5,35 +5,35 @@ import {h} from 'vue'
 const {$gettext} = gettext
 
 const configColumns = [{
-    title: () => $gettext('Name'),
-    dataIndex: 'name',
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Name'),
+  dataIndex: 'name',
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Type'),
-    dataIndex: 'is_dir',
-    customRender: (args: customRender) => {
-        const template: any = []
-        const {text, column} = args
-        if (text === true || text > 0) {
-            template.push($gettext('Dir'))
-        } else {
-            template.push($gettext('File'))
-        }
-        return h('div', template)
-    },
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Type'),
+  dataIndex: 'is_dir',
+  customRender: (args: customRender) => {
+    const template: any = []
+    const {text, column} = args
+    if (text === true || text > 0) {
+      template.push($gettext('Dir'))
+    } else {
+      template.push($gettext('File'))
+    }
+    return h('div', template)
+  },
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Updated at'),
-    dataIndex: 'modify',
-    customRender: datetime,
-    datetime: true,
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Updated at'),
+  dataIndex: 'modify',
+  customRender: datetime,
+  datetime: true,
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Action'),
-    dataIndex: 'action'
+  title: () => $gettext('Action'),
+  dataIndex: 'action'
 }]
 
 export default configColumns

+ 8 - 8
frontend/src/views/config/constants.ts

@@ -4,14 +4,14 @@ import {createEnum} from '@/lib/helper'
 // nginx log level: debug, info, notice, warn, error, crit, alert, or emerg
 
 const logLevel = createEnum({
-    Debug: [0, 'debug'],
-    Info: [1, 'info'],
-    Notice: [2, 'notice'],
-    Warn: [3, 'warn'],
-    Error: [4, 'error'],
-    Crit: [5, 'crit'],
-    Alert: [6, 'alert'],
-    Emerg: [7, 'emerg']
+  Debug: [0, 'debug'],
+  Info: [1, 'info'],
+  Notice: [2, 'notice'],
+  Warn: [3, 'warn'],
+  Error: [4, 'error'],
+  Crit: [5, 'crit'],
+  Alert: [6, 'alert'],
+  Emerg: [7, 'emerg']
 })
 
 export default logLevel

+ 4 - 4
frontend/src/views/dashboard/DashBoard.vue

@@ -4,10 +4,10 @@ import Environments from '@/views/dashboard/Environments.vue'
 </script>
 
 <template>
-    <div>
-        <server-analytic/>
-        <environments/>
-    </div>
+  <div>
+    <server-analytic/>
+    <environments/>
+  </div>
 </template>
 
 <style scoped lang="less">

+ 82 - 82
frontend/src/views/dashboard/Environments.vue

@@ -17,120 +17,120 @@ const {$gettext} = useGettext()
 const data = ref([])
 
 const node_map = computed(() => {
-    const o = {}
-    data.value.forEach(v => {
-        o[v.id] = v
-    })
-    return o
+  const o = {}
+  data.value.forEach(v => {
+    o[v.id] = v
+  })
+  return o
 })
 
 let websocket: ReconnectingWebSocket | WebSocket
 
 onMounted(() => {
-    environment.get_list().then(r => {
-        data.value = r.data
-    })
-    websocket = ws('/api/analytic/nodes')
-    websocket.onmessage = m => {
-        const nodes = JSON.parse(m.data)
-        for (let key in nodes) {
-            // update node online status
-            if (node_map.value[key]) {
-                Object.assign(node_map.value[key], nodes[key])
-                node_map.value[key].response_at = new Date()
-            }
-        }
+  environment.get_list().then(r => {
+    data.value = r.data
+  })
+  websocket = ws('/api/analytic/nodes')
+  websocket.onmessage = m => {
+    const nodes = JSON.parse(m.data)
+    for (let key in nodes) {
+      // update node online status
+      if (node_map.value[key]) {
+        Object.assign(node_map.value[key], nodes[key])
+        node_map.value[key].response_at = new Date()
+      }
     }
+  }
 })
 
 onUnmounted(() => {
-    websocket.close()
+  websocket.close()
 })
 
 export interface Node {
-    id: number
-    name: string
-    token: string
+  id: number
+  name: string
+  token: string
 }
 
 const {environment: env} = settingsStore
 
 function link_start(node: Node) {
-    env.id = node.id
-    env.name = node.name
+  env.id = node.id
+  env.name = node.name
 }
 
 const visible = computed(() => {
-    if (env.id > 0) {
-        return false
-    } else {
-        return data.value?.length
-    }
+  if (env.id > 0) {
+    return false
+  } else {
+    return data.value?.length
+  }
 })
 </script>
 
 <template>
-    <a-card class="env-list-card" :title="$gettext('Environments')" v-if="visible">
-        <a-list item-layout="horizontal" :data-source="data">
-            <template #renderItem="{ item }">
-                <a-list-item>
-                    <template #actions>
-                        <a-button type="primary" @click="link_start(item)" :disabled="env.id===item.id" ghost>
-                            <send-outlined/>
-                            {{ env.id !== item.id ? $gettext('Link Start') : $gettext('Connected') }}
-                        </a-button>
-                    </template>
-                    <a-list-item-meta>
-                        <template #title>
-                            {{ item.name }}
-                            <a-tag color="blue" v-if="item.status">{{ $gettext('Online') }}</a-tag>
-                            <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
-                            <div class="runtime-meta">
-                                <template v-if="item.status">
-                                    <span><Icon :component="pulse"/> {{ formatDateTime(item.response_at) }}</span>
-                                    <span><thunderbolt-outlined/>{{ item.version }}</span>
-                                </template>
-                                <span><link-outlined/>{{ item.url }}</span>
-                            </div>
-                        </template>
-                        <template #avatar>
-                            <a-avatar :src="logo"/>
-                        </template>
-                        <template #description>
-                            <node-analytic-item :item="item"/>
-                        </template>
-                    </a-list-item-meta>
-                </a-list-item>
+  <a-card class="env-list-card" :title="$gettext('Environments')" v-if="visible">
+    <a-list item-layout="horizontal" :data-source="data">
+      <template #renderItem="{ item }">
+        <a-list-item>
+          <template #actions>
+            <a-button type="primary" @click="link_start(item)" :disabled="env.id===item.id" ghost>
+              <send-outlined/>
+              {{ env.id !== item.id ? $gettext('Link Start') : $gettext('Connected') }}
+            </a-button>
+          </template>
+          <a-list-item-meta>
+            <template #title>
+              {{ item.name }}
+              <a-tag color="blue" v-if="item.status">{{ $gettext('Online') }}</a-tag>
+              <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
+              <div class="runtime-meta">
+                <template v-if="item.status">
+                  <span><Icon :component="pulse"/> {{ formatDateTime(item.response_at) }}</span>
+                  <span><thunderbolt-outlined/>{{ item.version }}</span>
+                </template>
+                <span><link-outlined/>{{ item.url }}</span>
+              </div>
+            </template>
+            <template #avatar>
+              <a-avatar :src="logo"/>
             </template>
-        </a-list>
-    </a-card>
+            <template #description>
+              <node-analytic-item :item="item"/>
+            </template>
+          </a-list-item-meta>
+        </a-list-item>
+      </template>
+    </a-list>
+  </a-card>
 </template>
 
 <style scoped lang="less">
 .env-list-card {
-    margin-top: 16px;
+  margin-top: 16px;
 
-    .runtime-meta {
-        display: inline-flex;
-        @media (max-width: 700px) {
-            display: block;
-            margin-top: 5px;
-            span {
-                display: flex;
-                align-items: center;
-            }
-        }
+  .runtime-meta {
+    display: inline-flex;
+    @media (max-width: 700px) {
+      display: block;
+      margin-top: 5px;
+      span {
+        display: flex;
+        align-items: center;
+      }
+    }
 
-        span {
-            font-weight: 400;
-            font-size: 13px;
-            margin-right: 16px;
-            color: #9b9b9b;
+    span {
+      font-weight: 400;
+      font-size: 13px;
+      margin-right: 16px;
+      color: #9b9b9b;
 
-            &.anticon {
-                margin-right: 4px;
-            }
-        }
+      &.anticon {
+        margin-right: 4px;
+      }
     }
+  }
 }
 </style>

+ 236 - 236
frontend/src/views/dashboard/ServerAnalytic.vue

@@ -14,20 +14,20 @@ const {$gettext} = useGettext()
 let websocket: ReconnectingWebSocket | WebSocket
 
 const host: any = reactive({
-    platform: '',
-    platformVersion: '',
-    os: '',
-    kernelVersion: '',
-    kernelArch: ''
+  platform: '',
+  platformVersion: '',
+  os: '',
+  kernelVersion: '',
+  kernelArch: ''
 })
 
 const cpu = ref('0.0')
 const cpu_info = reactive([])
 const cpu_analytic_series = reactive([{name: 'User', data: <any>[]}, {name: 'Total', data: <any>[]}])
 const net_analytic = reactive([{name: $gettext('Receive'), data: <any>[]},
-    {name: $gettext('Send'), data: <any>[]}])
+  {name: $gettext('Send'), data: <any>[]}])
 const disk_io_analytic = reactive([{name: $gettext('Writes'), data: <any>[]},
-    {name: $gettext('Reads'), data: <any>[]}])
+  {name: $gettext('Reads'), data: <any>[]}])
 const memory: any = reactive({swap_used: '', swap_percent: '', swap_total: ''})
 const disk: any = reactive({percentage: '', used: ''})
 const disk_io = reactive({writes: 0, reads: 0})
@@ -36,304 +36,304 @@ const loadavg = reactive({load1: 0, load5: 0, load15: 0})
 const net = reactive({recv: 0, sent: 0, last_recv: 0, last_sent: 0})
 
 const net_formatter = (bytes: number) => {
-    return bytesToSize(bytes) + '/s'
+  return bytesToSize(bytes) + '/s'
 }
 
 interface Usage {
-    x: number
-    y: number
+  x: number
+  y: number
 }
 
 onMounted(() => {
-    analytic.init().then(r => {
-        Object.assign(host, r.host)
-        Object.assign(cpu_info, r.cpu.info)
-        Object.assign(memory, r.memory)
-        Object.assign(disk, r.disk)
-
-        // uptime
-        handle_uptime(r.host?.uptime)
-        // load_avg
-        Object.assign(loadavg, r.loadavg)
-
-        net.last_recv = r.network.init.bytesRecv
-        net.last_sent = r.network.init.bytesSent
-        r.cpu.user.forEach((u: Usage) => {
-            cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
-        })
-        r.cpu.total.forEach((u: Usage) => {
-            cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
-        })
-        r.network.bytesRecv.forEach((u: Usage) => {
-            net_analytic[0].data.push([u.x, u.y.toFixed(2)])
-        })
-        r.network.bytesSent.forEach((u: Usage) => {
-            net_analytic[1].data.push([u.x, u.y.toFixed(2)])
-        })
-        disk_io_analytic[0].data = disk_io_analytic[0].data.concat(r.disk_io.writes)
-        disk_io_analytic[1].data = disk_io_analytic[1].data.concat(r.disk_io.reads)
-
-        websocket = ws('/api/analytic')
-        websocket.onmessage = wsOnMessage
+  analytic.init().then(r => {
+    Object.assign(host, r.host)
+    Object.assign(cpu_info, r.cpu.info)
+    Object.assign(memory, r.memory)
+    Object.assign(disk, r.disk)
+
+    // uptime
+    handle_uptime(r.host?.uptime)
+    // load_avg
+    Object.assign(loadavg, r.loadavg)
 
+    net.last_recv = r.network.init.bytesRecv
+    net.last_sent = r.network.init.bytesSent
+    r.cpu.user.forEach((u: Usage) => {
+      cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
+    })
+    r.cpu.total.forEach((u: Usage) => {
+      cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
+    })
+    r.network.bytesRecv.forEach((u: Usage) => {
+      net_analytic[0].data.push([u.x, u.y.toFixed(2)])
+    })
+    r.network.bytesSent.forEach((u: Usage) => {
+      net_analytic[1].data.push([u.x, u.y.toFixed(2)])
     })
+    disk_io_analytic[0].data = disk_io_analytic[0].data.concat(r.disk_io.writes)
+    disk_io_analytic[1].data = disk_io_analytic[1].data.concat(r.disk_io.reads)
+
+    websocket = ws('/api/analytic')
+    websocket.onmessage = wsOnMessage
+
+  })
 })
 
 onUnmounted(() => {
-    websocket.close()
+  websocket.close()
 })
 
 function handle_uptime(t: number) {
-    // uptime
-    let _uptime = Math.floor(t)
-    let uptime_days = Math.floor(_uptime / 86400)
-    _uptime -= uptime_days * 86400
-    let uptime_hours = Math.floor(_uptime / 3600)
-    _uptime -= uptime_hours * 3600
-    uptime.value = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(_uptime / 60) + 'm'
+  // uptime
+  let _uptime = Math.floor(t)
+  let uptime_days = Math.floor(_uptime / 86400)
+  _uptime -= uptime_days * 86400
+  let uptime_hours = Math.floor(_uptime / 3600)
+  _uptime -= uptime_hours * 3600
+  uptime.value = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(_uptime / 60) + 'm'
 }
 
 function wsOnMessage(m: { data: any }) {
-    const r = JSON.parse(m.data)
+  const r = JSON.parse(m.data)
 
-    const cpu_usage = r.cpu.system + r.cpu.user
-    cpu.value = cpu_usage.toFixed(2)
+  const cpu_usage = r.cpu.system + r.cpu.user
+  cpu.value = cpu_usage.toFixed(2)
 
-    const time = new Date().getTime()
+  const time = new Date().getTime()
 
-    cpu_analytic_series[0].data.push([time, r.cpu.user.toFixed(2)])
-    cpu_analytic_series[1].data.push([time, cpu.value])
+  cpu_analytic_series[0].data.push([time, r.cpu.user.toFixed(2)])
+  cpu_analytic_series[1].data.push([time, cpu.value])
 
-    if (cpu_analytic_series[0].data.length > 100) {
-        cpu_analytic_series[0].data.shift()
-        cpu_analytic_series[1].data.shift()
-    }
+  if (cpu_analytic_series[0].data.length > 100) {
+    cpu_analytic_series[0].data.shift()
+    cpu_analytic_series[1].data.shift()
+  }
 
-    // mem
-    Object.assign(memory, r.memory)
+  // mem
+  Object.assign(memory, r.memory)
 
-    // disk
-    Object.assign(disk, r.disk)
-    disk_io.writes = r.disk.writes.y
-    disk_io.reads = r.disk.reads.y
+  // disk
+  Object.assign(disk, r.disk)
+  disk_io.writes = r.disk.writes.y
+  disk_io.reads = r.disk.reads.y
 
-    // uptime
-    handle_uptime(r.uptime)
+  // uptime
+  handle_uptime(r.uptime)
 
-    // loadavg
-    Object.assign(loadavg, r.loadavg)
+  // loadavg
+  Object.assign(loadavg, r.loadavg)
 
-    // network
-    Object.assign(net, r.network)
-    net.recv = r.network.bytesRecv - net.last_recv
-    net.sent = r.network.bytesSent - net.last_sent
-    net.last_recv = r.network.bytesRecv
-    net.last_sent = r.network.bytesSent
+  // network
+  Object.assign(net, r.network)
+  net.recv = r.network.bytesRecv - net.last_recv
+  net.sent = r.network.bytesSent - net.last_sent
+  net.last_recv = r.network.bytesRecv
+  net.last_sent = r.network.bytesSent
 
-    net_analytic[0].data.push([time, net.recv])
-    net_analytic[1].data.push([time, net.sent])
+  net_analytic[0].data.push([time, net.recv])
+  net_analytic[1].data.push([time, net.sent])
 
-    if (net_analytic[0].data.length > 100) {
-        net_analytic[0].data.shift()
-        net_analytic[1].data.shift()
-    }
+  if (net_analytic[0].data.length > 100) {
+    net_analytic[0].data.shift()
+    net_analytic[1].data.shift()
+  }
 
-    disk_io_analytic[0].data.push(r.disk.writes)
-    disk_io_analytic[1].data.push(r.disk.reads)
+  disk_io_analytic[0].data.push(r.disk.writes)
+  disk_io_analytic[1].data.push(r.disk.reads)
 
-    if (disk_io_analytic[0].data.length > 100) {
-        disk_io_analytic[0].data.shift()
-        disk_io_analytic[1].data.shift()
-    }
+  if (disk_io_analytic[0].data.length > 100) {
+    disk_io_analytic[0].data.shift()
+    disk_io_analytic[1].data.shift()
+  }
 }
 </script>
 
 <template>
-    <div>
-        <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="first-row">
-            <a-col :xl="7" :lg="24" :md="24" :xs="24">
-                <a-card :title="$gettext('Server Info')" :bordered="false">
-                    <p>
-                        <translate>Uptime:</translate>
-                        {{ uptime }}
-                    </p>
-                    <p>
-                        <translate>Load Averages:</translate>
-                        <span class="load-avg-describe"> 1min:</span>{{ ' ' + loadavg?.load1?.toFixed(2) }}
-                        <span class="load-avg-describe"> | 5min:</span>{{ loadavg?.load5?.toFixed(2) }}
-                        <span class="load-avg-describe"> | 15min:</span>{{ loadavg?.load15?.toFixed(2) }}
-                    </p>
-                    <p>
-                        <translate>OS:</translate>
-                        <span class="os-platform">{{ ' ' + host.platform }}</span> {{ host.platformVersion }}
-                        <span class="os-info">({{ host.os }} {{ host.kernelVersion }}
+  <div>
+    <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="first-row">
+      <a-col :xl="7" :lg="24" :md="24" :xs="24">
+        <a-card :title="$gettext('Server Info')" :bordered="false">
+          <p>
+            <translate>Uptime:</translate>
+            {{ uptime }}
+          </p>
+          <p>
+            <translate>Load Averages:</translate>
+            <span class="load-avg-describe"> 1min:</span>{{ ' ' + loadavg?.load1?.toFixed(2) }}
+            <span class="load-avg-describe"> | 5min:</span>{{ loadavg?.load5?.toFixed(2) }}
+            <span class="load-avg-describe"> | 15min:</span>{{ loadavg?.load15?.toFixed(2) }}
+          </p>
+          <p>
+            <translate>OS:</translate>
+            <span class="os-platform">{{ ' ' + host.platform }}</span> {{ host.platformVersion }}
+            <span class="os-info">({{ host.os }} {{ host.kernelVersion }}
                         {{ host.kernelArch }})</span>
-                    </p>
-                    <p v-if="cpu_info">
-                        {{ $gettext('CPU:') + ' ' }}
-                        <span class="cpu-model">{{ cpu_info[0]?.modelName || 'Core' }}</span>
-                        <span class="cpu-mhz">{{
-                                cpu_info[0]?.mhz > 0.01 ? (cpu_info[0]?.mhz / 1000).toFixed(2) + 'GHz' : 'Core'
-                            }}</span>
-                        * {{ cpu_info.length }}
-                    </p>
-                </a-card>
+          </p>
+          <p v-if="cpu_info">
+            {{ $gettext('CPU:') + ' ' }}
+            <span class="cpu-model">{{ cpu_info[0]?.modelName || 'Core' }}</span>
+            <span class="cpu-mhz">{{
+                cpu_info[0]?.mhz > 0.01 ? (cpu_info[0]?.mhz / 1000).toFixed(2) + 'GHz' : 'Core'
+              }}</span>
+            * {{ cpu_info.length }}
+          </p>
+        </a-card>
+      </a-col>
+      <a-col :xl="10" :lg="16" :md="24" :xs="24" class="chart_dashboard">
+        <a-card :title="$gettext('Memory and Storage')" :bordered="false">
+          <a-row :gutter="[0,16]">
+            <a-col :xs="24" :sm="24" :md="8">
+              <radial-bar-chart :name="$gettext('Memory')" :series="[memory.pressure]"
+                                :centerText="memory.used" :bottom-text="memory.total" colors="#36a3eb"/>
+            </a-col>
+            <a-col :xs="24" :sm="12" :md="8">
+              <radial-bar-chart :name="$gettext('Swap')" :series="[memory.swap_percent]"
+                                :centerText="memory.swap_used"
+                                :bottom-text="memory.swap_total" colors="#ff6385"/>
             </a-col>
-            <a-col :xl="10" :lg="16" :md="24" :xs="24" class="chart_dashboard">
-                <a-card :title="$gettext('Memory and Storage')" :bordered="false">
-                    <a-row :gutter="[0,16]">
-                        <a-col :xs="24" :sm="24" :md="8">
-                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory.pressure]"
-                                              :centerText="memory.used" :bottom-text="memory.total" colors="#36a3eb"/>
-                        </a-col>
-                        <a-col :xs="24" :sm="12" :md="8">
-                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory.swap_percent]"
-                                              :centerText="memory.swap_used"
-                                              :bottom-text="memory.swap_total" colors="#ff6385"/>
-                        </a-col>
-                        <a-col :xs="24" :sm="12" :md="8">
-                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk.percentage]"
-                                              :centerText="disk.used" :bottom-text="disk.total" colors="#87d068"/>
-                        </a-col>
-                    </a-row>
-                </a-card>
+            <a-col :xs="24" :sm="12" :md="8">
+              <radial-bar-chart :name="$gettext('Storage')" :series="[disk.percentage]"
+                                :centerText="disk.used" :bottom-text="disk.total" colors="#87d068"/>
             </a-col>
-            <a-col :xl="7" :lg="8" :sm="24" :xs="24" class="chart_dashboard network-total">
-                <a-card :title="$gettext('Network Statistics')" :bordered="false">
-                    <a-row :gutter="16">
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.last_recv)"
-                                         :title="$gettext('Network Total Receive')"/>
-                        </a-col>
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.last_sent)"
-                                         :title="$gettext('Network Total Send')"/>
-                        </a-col>
-                    </a-row>
-                </a-card>
+          </a-row>
+        </a-card>
+      </a-col>
+      <a-col :xl="7" :lg="8" :sm="24" :xs="24" class="chart_dashboard network-total">
+        <a-card :title="$gettext('Network Statistics')" :bordered="false">
+          <a-row :gutter="16">
+            <a-col :span="12">
+              <a-statistic :value="bytesToSize(net.last_recv)"
+                           :title="$gettext('Network Total Receive')"/>
             </a-col>
-        </a-row>
-        <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="row-two">
-            <a-col :xl="8" :lg="24" :md="24" :sm="24" :xs="24">
-                <a-card :title="$gettext('CPU Status')" :bordered="false">
-                    <a-statistic :value="cpu" title="CPU">
-                        <template v-slot:suffix>
-                            <span>%</span>
-                        </template>
-                    </a-statistic>
-                    <area-chart :series="cpu_analytic_series" :max="100"/>
-                </a-card>
+            <a-col :span="12">
+              <a-statistic :value="bytesToSize(net.last_sent)"
+                           :title="$gettext('Network Total Send')"/>
             </a-col>
-            <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
-                <a-card :title="$gettext('Network')" :bordered="false">
-                    <a-row :gutter="16">
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.recv)"
-                                         :title="$gettext('Receive')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.sent)" :title="$gettext('Send')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                    </a-row>
-                    <area-chart :series="net_analytic" :y_formatter="net_formatter"/>
-                </a-card>
+          </a-row>
+        </a-card>
+      </a-col>
+    </a-row>
+    <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="row-two">
+      <a-col :xl="8" :lg="24" :md="24" :sm="24" :xs="24">
+        <a-card :title="$gettext('CPU Status')" :bordered="false">
+          <a-statistic :value="cpu" title="CPU">
+            <template v-slot:suffix>
+              <span>%</span>
+            </template>
+          </a-statistic>
+          <area-chart :series="cpu_analytic_series" :max="100"/>
+        </a-card>
+      </a-col>
+      <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
+        <a-card :title="$gettext('Network')" :bordered="false">
+          <a-row :gutter="16">
+            <a-col :span="12">
+              <a-statistic :value="bytesToSize(net.recv)"
+                           :title="$gettext('Receive')">
+                <template v-slot:suffix>
+                  <span>/s</span>
+                </template>
+              </a-statistic>
             </a-col>
-            <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
-                <a-card :title="$gettext('Disk IO')" :bordered="false">
-                    <a-row :gutter="16">
-                        <a-col :span="12">
-                            <a-statistic :value="disk_io.writes"
-                                         :title="$gettext('Writes')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                        <a-col :span="12">
-                            <a-statistic :value="disk_io.reads" :title="$gettext('Reads')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                    </a-row>
-                    <area-chart :series="disk_io_analytic"/>
-                </a-card>
+            <a-col :span="12">
+              <a-statistic :value="bytesToSize(net.sent)" :title="$gettext('Send')">
+                <template v-slot:suffix>
+                  <span>/s</span>
+                </template>
+              </a-statistic>
             </a-col>
-        </a-row>
-    </div>
+          </a-row>
+          <area-chart :series="net_analytic" :y_formatter="net_formatter"/>
+        </a-card>
+      </a-col>
+      <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
+        <a-card :title="$gettext('Disk IO')" :bordered="false">
+          <a-row :gutter="16">
+            <a-col :span="12">
+              <a-statistic :value="disk_io.writes"
+                           :title="$gettext('Writes')">
+                <template v-slot:suffix>
+                  <span>/s</span>
+                </template>
+              </a-statistic>
+            </a-col>
+            <a-col :span="12">
+              <a-statistic :value="disk_io.reads" :title="$gettext('Reads')">
+                <template v-slot:suffix>
+                  <span>/s</span>
+                </template>
+              </a-statistic>
+            </a-col>
+          </a-row>
+          <area-chart :series="disk_io_analytic"/>
+        </a-card>
+      </a-col>
+    </a-row>
+  </div>
 </template>
 
 <style lang="less" scoped>
 .first-row {
-    .ant-card {
-        min-height: 227px;
+  .ant-card {
+    min-height: 227px;
 
-        p {
-            margin-bottom: 8px;
-        }
+    p {
+      margin-bottom: 8px;
     }
+  }
 
-    margin-bottom: 20px;
+  margin-bottom: 20px;
 }
 
 .ant-card {
-    .ant-statistic {
-        margin: 0 0 10px 10px
-    }
+  .ant-statistic {
+    margin: 0 0 10px 10px
+  }
 
-    .chart {
-        max-width: 800px;
-        max-height: 350px;
-    }
+  .chart {
+    max-width: 800px;
+    max-height: 350px;
+  }
 
-    .chart_dashboard {
-        padding: 60px;
+  .chart_dashboard {
+    padding: 60px;
 
-        .description {
-            width: 120px;
-            text-align: center
-        }
+    .description {
+      width: 120px;
+      text-align: center
     }
+  }
 
-    @media (max-width: 512px) {
-        margin: 10px 0;
-        .chart_dashboard {
-            padding: 20px;
-        }
+  @media (max-width: 512px) {
+    margin: 10px 0;
+    .chart_dashboard {
+      padding: 20px;
     }
+  }
 }
 
 .load-avg-describe {
-    @media (max-width: 1600px) and (min-width: 1200px) {
-        display: none;
-    }
+  @media (max-width: 1600px) and (min-width: 1200px) {
+    display: none;
+  }
 }
 
 .os-info {
-    @media (max-width: 1600px) and (min-width: 1200px) {
-        display: none;
-    }
+  @media (max-width: 1600px) and (min-width: 1200px) {
+    display: none;
+  }
 }
 
 .cpu-model {
-    @media (max-width: 1790px) and (min-width: 1200px) {
-        display: none;
-    }
+  @media (max-width: 1790px) and (min-width: 1200px) {
+    display: none;
+  }
 }
 
 .cpu-mhz {
-    @media (min-width: 1790px) or (max-width: 1200px) {
-        display: none;
-    }
+  @media (min-width: 1790px) or (max-width: 1200px) {
+    display: none;
+  }
 }
 </style>
 

+ 55 - 55
frontend/src/views/dashboard/components/NodeAnalyticItem.vue

@@ -9,72 +9,72 @@ const props = defineProps(['item'])
 </script>
 
 <template>
-    <div class="hardware-monitor">
-        <div class="hardware-monitor-item longer">
-            <div>
-                <line-chart-outlined/>
-                <span class="load-avg-describe">1min:</span>{{ ' ' + item.avg_load?.load1?.toFixed(2) }} ·
-                <span class="load-avg-describe">5min:</span>{{ item.avg_load?.load5?.toFixed(2) }} ·
-                <span class="load-avg-describe">15min:</span>{{ item.avg_load?.load15?.toFixed(2) }}
-            </div>
-            <div>
-                <arrow-up-outlined/>
-                {{ bytesToSize(item?.network?.bytesSent) }}
-                <arrow-down-outlined/>
-                {{ bytesToSize(item?.network?.bytesRecv) }}
-            </div>
-        </div>
-        <div class="hardware-monitor-item">
-            <usage-progress-line :percent="item.cpu_percent">
-                <template #icon>
-                    <Icon :component="cpu"/>
-                </template>
-                <span>{{ item.cpu_num }} CPU</span>
-            </usage-progress-line>
-        </div>
-        <div class="hardware-monitor-item">
-            <usage-progress-line :percent="item.memory_percent">
-                <template #icon>
-                    <Icon :component="memory"/>
-                </template>
-                <span>{{ item.memory_total }}</span>
-            </usage-progress-line>
-        </div>
-        <div class="hardware-monitor-item">
-            <usage-progress-line :percent="item.disk_percent">
-                <template #icon>
-                    <database-outlined/>
-                </template>
-                <span>{{ item.disk_total }}</span>
-            </usage-progress-line>
-        </div>
+  <div class="hardware-monitor">
+    <div class="hardware-monitor-item longer">
+      <div>
+        <line-chart-outlined/>
+        <span class="load-avg-describe">1min:</span>{{ ' ' + item.avg_load?.load1?.toFixed(2) }} ·
+        <span class="load-avg-describe">5min:</span>{{ item.avg_load?.load5?.toFixed(2) }} ·
+        <span class="load-avg-describe">15min:</span>{{ item.avg_load?.load15?.toFixed(2) }}
+      </div>
+      <div>
+        <arrow-up-outlined/>
+        {{ bytesToSize(item?.network?.bytesSent) }}
+        <arrow-down-outlined/>
+        {{ bytesToSize(item?.network?.bytesRecv) }}
+      </div>
     </div>
+    <div class="hardware-monitor-item">
+      <usage-progress-line :percent="item.cpu_percent">
+        <template #icon>
+          <Icon :component="cpu"/>
+        </template>
+        <span>{{ item.cpu_num }} CPU</span>
+      </usage-progress-line>
+    </div>
+    <div class="hardware-monitor-item">
+      <usage-progress-line :percent="item.memory_percent">
+        <template #icon>
+          <Icon :component="memory"/>
+        </template>
+        <span>{{ item.memory_total }}</span>
+      </usage-progress-line>
+    </div>
+    <div class="hardware-monitor-item">
+      <usage-progress-line :percent="item.disk_percent">
+        <template #icon>
+          <database-outlined/>
+        </template>
+        <span>{{ item.disk_total }}</span>
+      </usage-progress-line>
+    </div>
+  </div>
 </template>
 
 <style scoped lang="less">
 .hardware-monitor {
-    display: flex;
+  display: flex;
 
-    @media (max-width: 900px) {
-        display: block;
-    }
+  @media (max-width: 900px) {
+    display: block;
+  }
 
-    .hardware-monitor-item {
-        width: 150px;
-        margin-right: 30px;
-        @media (max-width: 900px) {
-            margin-bottom: 5px;
-        }
+  .hardware-monitor-item {
+    width: 150px;
+    margin-right: 30px;
+    @media (max-width: 900px) {
+      margin-bottom: 5px;
     }
+  }
 
-    .longer {
-        width: 300px;
-    }
+  .longer {
+    width: 300px;
+  }
 }
 
 .load-avg-describe {
-    @media (max-width: 1200px) and  (min-width: 600px) {
-        display: none;
-    }
+  @media (max-width: 1200px) and  (min-width: 600px) {
+    display: none;
+  }
 }
 </style>

+ 109 - 109
frontend/src/views/domain/DomainAdd.vue

@@ -12,11 +12,11 @@ import {useRouter} from 'vue-router'
 const {$gettext, interpolate} = useGettext()
 
 const ngx_config = reactive({
-    name: '',
-    servers: [{
-        directives: [],
-        locations: []
-    }]
+  name: '',
+  servers: [{
+    directives: [],
+    locations: []
+  }]
 })
 
 const error = reactive({})
@@ -31,145 +31,145 @@ const update = ref(0)
 
 
 onMounted(() => {
-    init()
+  init()
 })
 
 function init() {
-    domain.get_template().then(r => {
-        Object.assign(ngx_config, r.tokenized)
-    })
+  domain.get_template().then(r => {
+    Object.assign(ngx_config, r.tokenized)
+  })
 }
 
 function save() {
-    return ngx.build_config(ngx_config).then(r => {
-        domain.save(ngx_config.name, {name: ngx_config.name, content: r.content, overwrite: true}).then(() => {
-            message.success($gettext('Saved successfully'))
-
-            domain.enable(ngx_config.name).then(() => {
-                message.success($gettext('Enabled successfully'))
-                window.scroll({top: 0, left: 0, behavior: 'smooth'})
-            }).catch(r => {
-                message.error(r.message ?? $gettext('Enable failed'), 5)
-            })
-
-        }).catch(r => {
-            message.error(interpolate($gettext('Save error %{msg}'), {msg: $gettext(r.message) ?? ''}), 5)
-        })
+  return ngx.build_config(ngx_config).then(r => {
+    domain.save(ngx_config.name, {name: ngx_config.name, content: r.content, overwrite: true}).then(() => {
+      message.success($gettext('Saved successfully'))
+
+      domain.enable(ngx_config.name).then(() => {
+        message.success($gettext('Enabled successfully'))
+        window.scroll({top: 0, left: 0, behavior: 'smooth'})
+      }).catch(r => {
+        message.error(r.message ?? $gettext('Enable failed'), 5)
+      })
+
+    }).catch(r => {
+      message.error(interpolate($gettext('Save error %{msg}'), {msg: $gettext(r.message) ?? ''}), 5)
     })
+  })
 }
 
 const router = useRouter()
 
 function goto_modify() {
-    router.push('/domain/' + ngx_config.name)
+  router.push('/domain/' + ngx_config.name)
 }
 
 function create_another() {
-    router.go(0)
+  router.go(0)
 }
 
 const has_server_name = computed(() => {
-    const servers = ngx_config.servers
-    for (const server_key in servers) {
-        for (const k in servers[server_key].directives) {
-            const v: any = servers[server_key].directives[k]
-            if (v.directive === 'server_name' && v.params.trim() !== '') {
-                return true
-            }
-        }
+  const servers = ngx_config.servers
+  for (const server_key in servers) {
+    for (const k in servers[server_key].directives) {
+      const v: any = servers[server_key].directives[k]
+      if (v.directive === 'server_name' && v.params.trim() !== '') {
+        return true
+      }
     }
+  }
 
-    return false
+  return false
 })
 
 provide('save_site_config', save)
 
 async function next() {
-    await save()
-    current_step.value++
+  await save()
+  current_step.value++
 }
 </script>
 
 <template>
-    <a-card :title="$gettext('Add Site')">
-        <div class="domain-add-container">
-            <a-steps :current="current_step" size="small">
-                <a-step :title="$gettext('Base information')"/>
-                <a-step :title="$gettext('Configure SSL')"/>
-                <a-step :title="$gettext('Finished')"/>
-            </a-steps>
-            <template v-if="current_step===0">
-                <a-form layout="vertical">
-                    <a-form-item :label="$gettext('Configuration Name')">
-                        <a-input v-model:value="ngx_config.name"/>
-                    </a-form-item>
-                </a-form>
-
-                <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
-                <br/>
-                <location-editor :locations="ngx_config.servers[0].locations"/>
-                <br/>
-                <a-alert
-                    v-if="!has_server_name"
-                    :message="$gettext('Warning')"
-                    type="warning"
-                    show-icon
-                >
-                    <template #description>
-                        <span v-translate>server_name parameter is required</span>
-                    </template>
-                </a-alert>
-                <br/>
-            </template>
-
-            <template v-else-if="current_step===1">
-
-                <ngx-config-editor
-                    ref="ngx-config-editor"
-                    :ngx_config="ngx_config"
-                    v-model:auto_cert="auto_cert"
-                    :enabled="enabled"
-                />
-
-                <br/>
-
-            </template>
-
-            <a-space v-if="current_step<2">
-                <a-button
-                    type="primary"
-                    @click="next"
-                    :disabled="!ngx_config.name||!has_server_name"
-                >
-                    <translate>Next</translate>
-                </a-button>
-            </a-space>
-            <a-result
-                v-else-if="current_step===2"
-                status="success"
-                :title="$gettext('Domain Config Created Successfully')"
-            >
-                <template #extra>
-                    <a-button type="primary" @click="goto_modify">
-                        <translate>Modify Config</translate>
-                    </a-button>
-                    <a-button @click="create_another">
-                        <translate>Create Another</translate>
-                    </a-button>
-                </template>
-            </a-result>
-
-        </div>
-    </a-card>
+  <a-card :title="$gettext('Add Site')">
+    <div class="domain-add-container">
+      <a-steps :current="current_step" size="small">
+        <a-step :title="$gettext('Base information')"/>
+        <a-step :title="$gettext('Configure SSL')"/>
+        <a-step :title="$gettext('Finished')"/>
+      </a-steps>
+      <template v-if="current_step===0">
+        <a-form layout="vertical">
+          <a-form-item :label="$gettext('Configuration Name')">
+            <a-input v-model:value="ngx_config.name"/>
+          </a-form-item>
+        </a-form>
+
+        <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
+        <br/>
+        <location-editor :locations="ngx_config.servers[0].locations"/>
+        <br/>
+        <a-alert
+          v-if="!has_server_name"
+          :message="$gettext('Warning')"
+          type="warning"
+          show-icon
+        >
+          <template #description>
+            <span v-translate>server_name parameter is required</span>
+          </template>
+        </a-alert>
+        <br/>
+      </template>
+
+      <template v-else-if="current_step===1">
+
+        <ngx-config-editor
+          ref="ngx-config-editor"
+          :ngx_config="ngx_config"
+          v-model:auto_cert="auto_cert"
+          :enabled="enabled"
+        />
+
+        <br/>
+
+      </template>
+
+      <a-space v-if="current_step<2">
+        <a-button
+          type="primary"
+          @click="next"
+          :disabled="!ngx_config.name||!has_server_name"
+        >
+          <translate>Next</translate>
+        </a-button>
+      </a-space>
+      <a-result
+        v-else-if="current_step===2"
+        status="success"
+        :title="$gettext('Domain Config Created Successfully')"
+      >
+        <template #extra>
+          <a-button type="primary" @click="goto_modify">
+            <translate>Modify Config</translate>
+          </a-button>
+          <a-button @click="create_another">
+            <translate>Create Another</translate>
+          </a-button>
+        </template>
+      </a-result>
+
+    </div>
+  </a-card>
 </template>
 
 <style lang="less" scoped>
 .ant-steps {
-    padding: 10px 0 20px 0;
+  padding: 10px 0 20px 0;
 }
 
 .domain-add-container {
-    max-width: 800px;
-    margin: 0 auto
+  max-width: 800px;
+  margin: 0 auto
 }
 </style>

+ 173 - 173
frontend/src/views/domain/DomainEdit.vue

@@ -19,15 +19,15 @@ const router = useRouter()
 
 const name = ref(route.params.name.toString())
 watch(route, () => {
-    name.value = route.params?.name?.toString() ?? ''
+  name.value = route.params?.name?.toString() ?? ''
 })
 
 const update = ref(0)
 
 const ngx_config: any = reactive({
-    name: '',
-    upstreams: [],
-    servers: []
+  name: '',
+  upstreams: [],
+  servers: []
 })
 
 const cert_info_map: any = reactive({})
@@ -46,108 +46,108 @@ const data = ref({})
 init()
 
 const advance_mode = computed({
-    get() {
-        return advance_mode_ref.value || parse_error_status.value
-    },
-    set(v: boolean) {
-        advance_mode_ref.value = v
-    }
+  get() {
+    return advance_mode_ref.value || parse_error_status.value
+  },
+  set(v: boolean) {
+    advance_mode_ref.value = v
+  }
 })
 const history_chatgpt_record = ref([])
 
 function handle_response(r: any) {
-    if (r.advanced) {
-        advance_mode.value = true
-    }
-
-    if (r.advanced) {
-        advance_mode.value = true
-    }
-
-    Object.keys(cert_info_map).forEach(v => {
-        delete cert_info_map[v]
-    })
-    parse_error_status.value = false
-    parse_error_message.value = ''
-    filename.value = r.name
-    configText.value = r.config
-    enabled.value = r.enabled
-    auto_cert.value = r.auto_cert
-    history_chatgpt_record.value = r.chatgpt_messages
-    data.value = r
-    Object.assign(ngx_config, r.tokenized)
-    Object.assign(cert_info_map, r.cert_info)
+  if (r.advanced) {
+    advance_mode.value = true
+  }
+
+  if (r.advanced) {
+    advance_mode.value = true
+  }
+
+  Object.keys(cert_info_map).forEach(v => {
+    delete cert_info_map[v]
+  })
+  parse_error_status.value = false
+  parse_error_message.value = ''
+  filename.value = r.name
+  configText.value = r.config
+  enabled.value = r.enabled
+  auto_cert.value = r.auto_cert
+  history_chatgpt_record.value = r.chatgpt_messages
+  data.value = r
+  Object.assign(ngx_config, r.tokenized)
+  Object.assign(cert_info_map, r.cert_info)
 }
 
 function init() {
-    if (name.value) {
-        domain.get(name.value).then((r: any) => {
-            handle_response(r)
-        }).catch(handle_parse_error)
-    } else {
-        history_chatgpt_record.value = []
-    }
+  if (name.value) {
+    domain.get(name.value).then((r: any) => {
+      handle_response(r)
+    }).catch(handle_parse_error)
+  } else {
+    history_chatgpt_record.value = []
+  }
 }
 
 function handle_parse_error(r: any) {
-    if (r?.error === 'nginx_config_syntax_error') {
-        parse_error_status.value = true
-        parse_error_message.value = r.message
-        config.get('sites-available/' + name.value).then(r => {
-            configText.value = r.config
-        })
-    } else {
-        message.error($gettext(r?.message ?? 'Server error'))
-    }
+  if (r?.error === 'nginx_config_syntax_error') {
+    parse_error_status.value = true
+    parse_error_message.value = r.message
+    config.get('sites-available/' + name.value).then(r => {
+      configText.value = r.config
+    })
+  } else {
+    message.error($gettext(r?.message ?? 'Server error'))
+  }
 
-    throw r
+  throw r
 }
 
 function on_mode_change(advanced: boolean) {
-    domain.advance_mode(name.value, {advanced}).then(() => {
-        advance_mode.value = advanced
-        if (advanced) {
-            build_config()
-        } else {
-            return ngx.tokenize_config(configText.value).then((r: any) => {
-                Object.assign(ngx_config, r)
-            }).catch(handle_parse_error)
-        }
-    })
+  domain.advance_mode(name.value, {advanced}).then(() => {
+    advance_mode.value = advanced
+    if (advanced) {
+      build_config()
+    } else {
+      return ngx.tokenize_config(configText.value).then((r: any) => {
+        Object.assign(ngx_config, r)
+      }).catch(handle_parse_error)
+    }
+  })
 }
 
 function build_config() {
-    return ngx.build_config(ngx_config).then((r: any) => {
-        configText.value = r.content
-    })
+  return ngx.build_config(ngx_config).then((r: any) => {
+    configText.value = r.content
+  })
 }
 
 const save = async () => {
-    saving.value = true
-
-    if (!advance_mode.value) {
-        try {
-            await build_config()
-        } catch (e) {
-            saving.value = false
-            message.error($gettext('Failed to save, syntax error(s) was detected in the configuration.'))
-            return
-        }
+  saving.value = true
+
+  if (!advance_mode.value) {
+    try {
+      await build_config()
+    } catch (e) {
+      saving.value = false
+      message.error($gettext('Failed to save, syntax error(s) was detected in the configuration.'))
+      return
     }
-
-    await domain.save(name.value, {
-        name: filename.value || name.value,
-        content: configText.value, overwrite: true
-    }).then(r => {
-        handle_response(r)
-        router.push({
-            path: '/domain/' + filename.value,
-            query: route.query
-        })
-        message.success($gettext('Saved successfully'))
-    }).catch(handle_parse_error).finally(() => {
-        saving.value = false
+  }
+
+  await domain.save(name.value, {
+    name: filename.value || name.value,
+    content: configText.value, overwrite: true
+  }).then(r => {
+    handle_response(r)
+    router.push({
+      path: '/domain/' + filename.value,
+      query: route.query
     })
+    message.success($gettext('Saved successfully'))
+  }).catch(handle_parse_error).finally(() => {
+    saving.value = false
+  })
 }
 
 provide('save_site_config', save)
@@ -160,76 +160,76 @@ provide('filename', filename)
 provide('data', data)
 </script>
 <template>
-    <a-row :gutter="16">
-        <a-col :xs="24" :sm="24" :md="18">
-            <a-card :bordered="false">
-                <template #title>
-                    <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), {n: name}) }}</span>
-                    <a-tag color="blue" v-if="enabled">
-                        {{ $gettext('Enabled') }}
-                    </a-tag>
-                    <a-tag color="orange" v-else>
-                        {{ $gettext('Disabled') }}
-                    </a-tag>
-                </template>
-                <template #extra>
-                    <div class="mode-switch">
-                        <div class="switch">
-                            <a-switch size="small" :disabled="parse_error_status"
-                                      :checked="advance_mode" @change="on_mode_change"/>
-                        </div>
-                        <template v-if="advance_mode">
-                            <div>{{ $gettext('Advance Mode') }}</div>
-                        </template>
-                        <template v-else>
-                            <div>{{ $gettext('Basic Mode') }}</div>
-                        </template>
-                    </div>
-                </template>
-
-                <transition name="slide-fade">
-                    <div v-if="advance_mode" key="advance">
-                        <div class="parse-error-alert-wrapper" v-if="parse_error_status">
-                            <a-alert :message="$gettext('Nginx Configuration Parse Error')"
-                                     :description="parse_error_message"
-                                     type="error"
-                                     show-icon
-                            />
-                        </div>
-                        <div>
-                            <code-editor v-model:content="configText"/>
-                        </div>
-                    </div>
-
-                    <div class="domain-edit-container" key="basic" v-else>
-                        <ngx-config-editor
-                            ref="ngx_config_editor"
-                            :ngx_config="ngx_config"
-                            :cert_info="cert_info_map"
-                            v-model:auto_cert="auto_cert"
-                            :enabled="enabled"
-                            @callback="save()"
-                        />
-                    </div>
-                </transition>
-            </a-card>
-        </a-col>
-
-        <a-col class="col-right" :xs="24" :sm="24" :md="6">
-            <right-settings/>
-        </a-col>
-
-        <footer-tool-bar>
-            <a-space>
-                <a-button @click="$router.push('/domain/list')">
-                    <translate>Back</translate>
-                </a-button>
-                <a-button type="primary" @click="save" :loading="saving">
-                    <translate>Save</translate>
-                </a-button>
-            </a-space>
-        </footer-tool-bar>
-    </a-row>
+  <a-row :gutter="16">
+    <a-col :xs="24" :sm="24" :md="18">
+      <a-card :bordered="false">
+        <template #title>
+          <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), {n: name}) }}</span>
+          <a-tag color="blue" v-if="enabled">
+            {{ $gettext('Enabled') }}
+          </a-tag>
+          <a-tag color="orange" v-else>
+            {{ $gettext('Disabled') }}
+          </a-tag>
+        </template>
+        <template #extra>
+          <div class="mode-switch">
+            <div class="switch">
+              <a-switch size="small" :disabled="parse_error_status"
+                        :checked="advance_mode" @change="on_mode_change"/>
+            </div>
+            <template v-if="advance_mode">
+              <div>{{ $gettext('Advance Mode') }}</div>
+            </template>
+            <template v-else>
+              <div>{{ $gettext('Basic Mode') }}</div>
+            </template>
+          </div>
+        </template>
+
+        <transition name="slide-fade">
+          <div v-if="advance_mode" key="advance">
+            <div class="parse-error-alert-wrapper" v-if="parse_error_status">
+              <a-alert :message="$gettext('Nginx Configuration Parse Error')"
+                       :description="parse_error_message"
+                       type="error"
+                       show-icon
+              />
+            </div>
+            <div>
+              <code-editor v-model:content="configText"/>
+            </div>
+          </div>
+
+          <div class="domain-edit-container" key="basic" v-else>
+            <ngx-config-editor
+              ref="ngx_config_editor"
+              :ngx_config="ngx_config"
+              :cert_info="cert_info_map"
+              v-model:auto_cert="auto_cert"
+              :enabled="enabled"
+              @callback="save()"
+            />
+          </div>
+        </transition>
+      </a-card>
+    </a-col>
+
+    <a-col class="col-right" :xs="24" :sm="24" :md="6">
+      <right-settings/>
+    </a-col>
+
+    <footer-tool-bar>
+      <a-space>
+        <a-button @click="$router.push('/domain/list')">
+          <translate>Back</translate>
+        </a-button>
+        <a-button type="primary" @click="save" :loading="saving">
+          <translate>Save</translate>
+        </a-button>
+      </a-space>
+    </footer-tool-bar>
+  </a-row>
 </template>
 
 <style lang="less">
@@ -238,45 +238,45 @@ provide('data', data)
 
 <style lang="less" scoped>
 .col-right {
-    position: relative;
+  position: relative;
 }
 
 .ant-card {
-    margin: 10px 0;
-    box-shadow: unset;
+  margin: 10px 0;
+  box-shadow: unset;
 }
 
 .mode-switch {
-    display: flex;
+  display: flex;
 
-    .switch {
-        display: flex;
-        align-items: center;
-        margin-right: 5px;
-    }
+  .switch {
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+  }
 }
 
 .parse-error-alert-wrapper {
-    margin-bottom: 20px;
+  margin-bottom: 20px;
 }
 
 .domain-edit-container {
-    max-width: 800px;
-    margin: 0 auto;
+  max-width: 800px;
+  margin: 0 auto;
 }
 
 .slide-fade-enter-active {
-    transition: all .3s ease-in-out;
+  transition: all .3s ease-in-out;
 }
 
 .slide-fade-leave-active {
-    transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
+  transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
 }
 
 .slide-fade-enter-from, .slide-fade-enter-to, .slide-fade-leave-to
-    /* .slide-fade-leave-active for below version 2.1.8 */ {
-    transform: translateX(10px);
-    opacity: 0;
+  /* .slide-fade-leave-active for below version 2.1.8 */ {
+  transform: translateX(10px);
+  opacity: 0;
 }
 
 .location-block {
@@ -284,10 +284,10 @@ provide('data', data)
 }
 
 .directive-params-wrapper {
-    margin: 10px 0;
+  margin: 10px 0;
 }
 
 .tab-content {
-    padding: 10px;
+  padding: 10px;
 }
 </style>

+ 90 - 90
frontend/src/views/domain/DomainList.vue

@@ -12,76 +12,76 @@ import SiteDuplicate from '@/views/domain/components/SiteDuplicate.vue'
 const {$gettext, interpolate} = useGettext()
 
 const columns = [{
-    title: () => $gettext('Name'),
-    dataIndex: 'name',
-    sorter: true,
-    pithy: true,
-    edit: {
-        type: input
-    },
-    search: true
+  title: () => $gettext('Name'),
+  dataIndex: 'name',
+  sorter: true,
+  pithy: true,
+  edit: {
+    type: input
+  },
+  search: true
 }, {
-    title: () => $gettext('Status'),
-    dataIndex: 'enabled',
-    customRender: (args: customRender) => {
-        const template: any = []
-        const {text} = args
-        if (text === true || text > 0) {
-            template.push(<Badge status="success"/>)
-            template.push($gettext('Enable'))
-        } else {
-            template.push(<Badge status="warning"/>)
-            template.push($gettext('Disable'))
-        }
-        return h('div', template)
-    },
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Status'),
+  dataIndex: 'enabled',
+  customRender: (args: customRender) => {
+    const template: any = []
+    const {text} = args
+    if (text === true || text > 0) {
+      template.push(<Badge status="success"/>)
+      template.push($gettext('Enable'))
+    } else {
+      template.push(<Badge status="warning"/>)
+      template.push($gettext('Disable'))
+    }
+    return h('div', template)
+  },
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Updated at'),
-    dataIndex: 'modify',
-    customRender: datetime,
-    sorter: true,
-    pithy: true
+  title: () => $gettext('Updated at'),
+  dataIndex: 'modify',
+  customRender: datetime,
+  sorter: true,
+  pithy: true
 }, {
-    title: () => $gettext('Action'),
-    dataIndex: 'action'
+  title: () => $gettext('Action'),
+  dataIndex: 'action'
 }]
 
 const table = ref(null)
 
 interface Table {
-    get_list(): void
+  get_list(): void
 }
 
 function enable(name: any) {
-    domain.enable(name).then(() => {
-        message.success($gettext('Enabled successfully'))
-        const t: Table | null = table.value
-        t!.get_list()
-    }).catch(r => {
-        message.error(interpolate($gettext('Failed to enable %{msg}'), {msg: r.message ?? ''}), 10)
-    })
+  domain.enable(name).then(() => {
+    message.success($gettext('Enabled successfully'))
+    const t: Table | null = table.value
+    t!.get_list()
+  }).catch(r => {
+    message.error(interpolate($gettext('Failed to enable %{msg}'), {msg: r.message ?? ''}), 10)
+  })
 }
 
 function disable(name: any) {
-    domain.disable(name).then(() => {
-        message.success($gettext('Disabled successfully'))
-        const t: Table | null = table.value
-        t!.get_list()
-    }).catch(r => {
-        message.error(interpolate($gettext('Failed to disable %{msg}'), {msg: r.message ?? ''}))
-    })
+  domain.disable(name).then(() => {
+    message.success($gettext('Disabled successfully'))
+    const t: Table | null = table.value
+    t!.get_list()
+  }).catch(r => {
+    message.error(interpolate($gettext('Failed to disable %{msg}'), {msg: r.message ?? ''}))
+  })
 }
 
 function destroy(site_name: any) {
-    domain.destroy(site_name).then(() => {
-        const t: Table | null = table.value
-        t!.get_list()
-        message.success(interpolate($gettext('Delete site: %{site_name}'), {site_name: site_name}))
-    }).catch((e: any) => {
-        message.error(e?.message ?? $gettext('Server error'))
-    })
+  domain.destroy(site_name).then(() => {
+    const t: Table | null = table.value
+    t!.get_list()
+    message.success(interpolate($gettext('Delete site: %{site_name}'), {site_name: site_name}))
+  }).catch((e: any) => {
+    message.error(e?.message ?? $gettext('Server error'))
+  })
 }
 
 const show_duplicator = ref(false)
@@ -89,49 +89,49 @@ const show_duplicator = ref(false)
 const target = ref('')
 
 function handle_click_duplicate(name: string) {
-    show_duplicator.value = true
-    target.value = name
+  show_duplicator.value = true
+  target.value = name
 }
 </script>
 
 <template>
-    <a-card :title="$gettext('Manage Sites')">
-        <std-table
-            :api="domain"
-            :columns="columns"
-            row-key="name"
-            ref="table"
-            @clickEdit="r => $router.push({
+  <a-card :title="$gettext('Manage Sites')">
+    <std-table
+      :api="domain"
+      :columns="columns"
+      row-key="name"
+      ref="table"
+      @clickEdit="r => $router.push({
                 path: '/domain/' + r
             })"
-            :deletable="false"
-        >
-            <template #actions="{record}">
-                <a-divider type="vertical"/>
-                <a-button type="link" size="small" v-if="record.enabled" @click="disable(record.name)">
-                    {{ $gettext('Disabled') }}
-                </a-button>
-                <a-button type="link" size="small" v-else @click="enable(record.name)">
-                    {{ $gettext('Enabled') }}
-                </a-button>
-                <a-divider type="vertical"/>
-                <a-button type="link" size="small" @click="handle_click_duplicate(record.name)">
-                    {{ $gettext('Duplicate') }}
-                </a-button>
-                <a-divider type="vertical"/>
-                <a-popconfirm
-                    :cancelText="$gettext('No')"
-                    :okText="$gettext('OK')"
-                    :title="$gettext('Are you sure you want to delete?')"
-                    @confirm="destroy(record['name'])">
-                    <a-button type="link" size="small" :disabled="record.enabled">
-                        {{ $gettext('Delete') }}
-                    </a-button>
-                </a-popconfirm>
-            </template>
-        </std-table>
-        <site-duplicate v-model:visible="show_duplicator" :name="target" @duplicated="table.get_list()"/>
-    </a-card>
+      :deletable="false"
+    >
+      <template #actions="{record}">
+        <a-divider type="vertical"/>
+        <a-button type="link" size="small" v-if="record.enabled" @click="disable(record.name)">
+          {{ $gettext('Disabled') }}
+        </a-button>
+        <a-button type="link" size="small" v-else @click="enable(record.name)">
+          {{ $gettext('Enabled') }}
+        </a-button>
+        <a-divider type="vertical"/>
+        <a-button type="link" size="small" @click="handle_click_duplicate(record.name)">
+          {{ $gettext('Duplicate') }}
+        </a-button>
+        <a-divider type="vertical"/>
+        <a-popconfirm
+          :cancelText="$gettext('No')"
+          :okText="$gettext('OK')"
+          :title="$gettext('Are you sure you want to delete?')"
+          @confirm="destroy(record['name'])">
+          <a-button type="link" size="small" :disabled="record.enabled">
+            {{ $gettext('Delete') }}
+          </a-button>
+        </a-popconfirm>
+      </template>
+    </std-table>
+    <site-duplicate v-model:visible="show_duplicator" :name="target" @duplicated="table.get_list()"/>
+  </a-card>
 </template>
 
 <style scoped>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio