Browse Source

[frontend-next] Refactored configs, terminal

0xJacky 2 years ago
parent
commit
d2fb41fbdf
57 changed files with 1565 additions and 1261 deletions
  1. 3 1
      frontend-next/.vscode/extensions.json
  2. 10 3
      frontend-next/README.md
  3. 9 0
      frontend-next/components.d.ts
  4. 5 0
      frontend-next/index.html
  5. 3 1
      frontend-next/package.json
  6. 18 1
      frontend-next/public/vite.svg
  7. 3 5
      frontend-next/src/App.vue
  8. 1 1
      frontend-next/src/api/analytic.ts
  9. 2 2
      frontend-next/src/api/auth.ts
  10. 5 0
      frontend-next/src/api/config.ts
  11. 34 0
      frontend-next/src/api/curd.ts
  12. 32 0
      frontend-next/src/api/domain.ts
  13. 13 0
      frontend-next/src/api/ngx.ts
  14. 5 0
      frontend-next/src/api/user.ts
  15. 8 11
      frontend-next/src/components/Breadcrumb/Breadcrumb.vue
  16. 4 4
      frontend-next/src/components/Chart/AreaChart.vue
  17. 1 2
      frontend-next/src/components/Chart/RadialBarChart.vue
  18. 19 0
      frontend-next/src/components/CodeEditor/CodeEditor.vue
  19. 1 2
      frontend-next/src/components/Logo/Logo.vue
  20. 4 21
      frontend-next/src/components/PageHeader/PageHeader.vue
  21. 0 3
      frontend-next/src/components/PageHeader/index.js
  22. 8 6
      frontend-next/src/components/SetLanguage/SetLanguage.vue
  23. 130 190
      frontend-next/src/components/StdDataDisplay/StdCurd.vue
  24. 239 328
      frontend-next/src/components/StdDataDisplay/StdTable.vue
  25. 17 0
      frontend-next/src/components/StdDataDisplay/StdTableTransformer.tsx
  26. 0 0
      frontend-next/src/components/StdDataDisplay/index.ts
  27. 5 5
      frontend-next/src/gettext.ts
  28. 0 0
      frontend-next/src/language/translations.json
  29. 11 6
      frontend-next/src/layouts/HeaderLayout.vue
  30. 7 10
      frontend-next/src/layouts/SideBar.vue
  31. 2 2
      frontend-next/src/lib/http/index.ts
  32. 15 15
      frontend-next/src/lib/theme/index.ts
  33. 3 3
      frontend-next/src/lib/websocket/index.ts
  34. 4 4
      frontend-next/src/main.ts
  35. 1 1
      frontend-next/src/pinia/settings.ts
  36. 1 1
      frontend-next/src/pinia/user.ts
  37. 38 38
      frontend-next/src/routes/index.ts
  38. 1 0
      frontend-next/src/style.less
  39. 27 36
      frontend-next/src/views/config/Config.vue
  40. 43 55
      frontend-next/src/views/config/ConfigEdit.vue
  41. 4 4
      frontend-next/src/views/dashboard/DashBoard.vue
  42. 117 131
      frontend-next/src/views/domain/DomainEdit.vue
  43. 73 68
      frontend-next/src/views/domain/DomainList.vue
  44. 1 1
      frontend-next/src/views/domain/ngx_conf/ngx_constant.js
  45. 3 2
      frontend-next/src/views/other/About.vue
  46. 3 2
      frontend-next/src/views/other/Install.vue
  47. 9 10
      frontend-next/src/views/other/Login.vue
  48. 2 2
      frontend-next/src/views/pty/Terminal.vue
  49. 11 19
      frontend-next/src/views/user/User.vue
  50. 3 3
      frontend-next/src/vite-env.d.ts
  51. 3 1
      frontend-next/tsconfig.json
  52. 9 7
      frontend-next/tsconfig.node.json
  53. 5 3
      frontend-next/vite.config.ts
  54. 352 13
      frontend-next/yarn.lock
  55. 2 2
      server/api/analytic.go
  56. 1 1
      server/api/config.go
  57. 235 235
      server/api/domain.go

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

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

+ 10 - 3
frontend-next/README.md

@@ -1,6 +1,8 @@
 # Vue 3 + TypeScript + Vite
 # Vue 3 + TypeScript + Vite
 
 
-This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue
+3 `<script setup>` SFCs, check out
+the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
 
 
 ## Recommended IDE Setup
 ## Recommended IDE Setup
 
 
@@ -8,9 +10,14 @@ This template should help get you started developing with Vue 3 and TypeScript i
 
 
 ## Type Support For `.vue` Imports in TS
 ## Type Support For `.vue` Imports in TS
 
 
-Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
+Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type
+by default. In most cases this is fine if you don't really care about component prop types outside of templates.
+However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using
+manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
 
 
-1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
+1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look
+   for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default,
+   Take Over mode will enable itself if the default TypeScript extension is disabled.
 2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
 2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
 
 
 You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
 You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

+ 9 - 0
frontend-next/components.d.ts

@@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
     ACard: typeof import('ant-design-vue/es')['Card']
     ACard: typeof import('ant-design-vue/es')['Card']
     ACol: typeof import('ant-design-vue/es')['Col']
     ACol: typeof import('ant-design-vue/es')['Col']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
     AForm: typeof import('ant-design-vue/es')['Form']
     AForm: typeof import('ant-design-vue/es')['Form']
     AFormItem: typeof import('ant-design-vue/es')['FormItem']
     AFormItem: typeof import('ant-design-vue/es')['FormItem']
@@ -26,13 +27,21 @@ declare module '@vue/runtime-core' {
     ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
     ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
+    AModal: typeof import('ant-design-vue/es')['Modal']
+    APagination: typeof import('ant-design-vue/es')['Pagination']
+    APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
     AreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     AreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     ARow: typeof import('ant-design-vue/es')['Row']
     ARow: typeof import('ant-design-vue/es')['Row']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
+    ASpace: typeof import('ant-design-vue/es')['Space']
     AStatistic: typeof import('ant-design-vue/es')['Statistic']
     AStatistic: typeof import('ant-design-vue/es')['Statistic']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
+    ASwitch: typeof import('ant-design-vue/es')['Switch']
+    ATable: typeof import('ant-design-vue/es')['Table']
+    ATag: typeof import('ant-design-vue/es')['Tag']
     Breadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
     Breadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
+    CodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
     FooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
     FooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
     Logo: typeof import('./src/components/Logo/Logo.vue')['default']
     Logo: typeof import('./src/components/Logo/Logo.vue')['default']
     PageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']
     PageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']

+ 5 - 0
frontend-next/index.html

@@ -4,6 +4,11 @@
     <meta charset="UTF-8"/>
     <meta charset="UTF-8"/>
     <link href="/favicon.ico" rel="icon">
     <link href="/favicon.ico" rel="icon">
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <style type="text/css">
+        #app {
+            height: 100%;
+        }
+    </style>
     <title><%- title %></title>
     <title><%- title %></title>
 </head>
 </head>
 <body>
 <body>

+ 3 - 1
frontend-next/package.json

@@ -15,8 +15,8 @@
         "ant-design-vue": "^3.2.10",
         "ant-design-vue": "^3.2.10",
         "apexcharts": "^3.35.4",
         "apexcharts": "^3.35.4",
         "axios": "^0.27.2",
         "axios": "^0.27.2",
+        "dayjs": "^1.11.4",
         "lodash": "^4.17.21",
         "lodash": "^4.17.21",
-        "moment": "^2.29.4",
         "path": "^0.12.7",
         "path": "^0.12.7",
         "pinia": "^2.0.17",
         "pinia": "^2.0.17",
         "pinia-plugin-persistedstate": "^1.6.3",
         "pinia-plugin-persistedstate": "^1.6.3",
@@ -24,6 +24,7 @@
         "vue": "^3.2.37",
         "vue": "^3.2.37",
         "vue-chartjs": "^4.1.1",
         "vue-chartjs": "^4.1.1",
         "vue-router": "4",
         "vue-router": "4",
+        "vue3-ace-editor": "^2.2.2",
         "vue3-apexcharts": "^1.4.1",
         "vue3-apexcharts": "^1.4.1",
         "vue3-gettext": "^2.3.0",
         "vue3-gettext": "^2.3.0",
         "vuex": "^4.0.2",
         "vuex": "^4.0.2",
@@ -34,6 +35,7 @@
     "devDependencies": {
     "devDependencies": {
         "@types/lodash": "^4.14.182",
         "@types/lodash": "^4.14.182",
         "@vitejs/plugin-vue": "^3.0.0",
         "@vitejs/plugin-vue": "^3.0.0",
+        "@vitejs/plugin-vue-jsx": "^2.0.0",
         "@zougt/vite-plugin-theme-preprocessor": "^1.4.5",
         "@zougt/vite-plugin-theme-preprocessor": "^1.4.5",
         "less": "^4.1.3",
         "less": "^4.1.3",
         "typescript": "^4.6.4",
         "typescript": "^4.6.4",

+ 18 - 1
frontend-next/public/vite.svg

@@ -1 +1,18 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
+<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img"
+     class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257">
+    <defs>
+        <linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%">
+            <stop offset="0%" stop-color="#41D1FF"></stop>
+            <stop offset="100%" stop-color="#BD34FE"></stop>
+        </linearGradient>
+        <linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%">
+            <stop offset="0%" stop-color="#FFEA83"></stop>
+            <stop offset="8.333%" stop-color="#FFDD35"></stop>
+            <stop offset="100%" stop-color="#FFA800"></stop>
+        </linearGradient>
+    </defs>
+    <path fill="url(#IconifyId1813088fe1fbc01fb466)"
+          d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path>
+    <path fill="url(#IconifyId1813088fe1fbc01fb467)"
+          d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path>
+</svg>

+ 3 - 5
frontend-next/src/App.vue

@@ -2,8 +2,8 @@
 // This starter template is using Vue 3 <script setup> SFCs
 // This starter template is using Vue 3 <script setup> SFCs
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
 //@ts-ignore
 //@ts-ignore
-import {useSettingsStore} from "@/pinia/settings"
-import {dark_mode} from "@/lib/theme"
+import {useSettingsStore} from '@/pinia/settings'
+import {dark_mode} from '@/lib/theme'
 
 
 let media = window.matchMedia('(prefers-color-scheme: dark)')
 let media = window.matchMedia('(prefers-color-scheme: dark)')
 const callback = (media: { matches: any; }) => {
 const callback = (media: { matches: any; }) => {
@@ -30,7 +30,5 @@ if (typeof media.addEventListener === 'function') {
 </template>
 </template>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
-#app {
-    height: 100%;
-}
+
 </style>
 </style>

+ 1 - 1
frontend-next/src/api/analytic.ts

@@ -1,4 +1,4 @@
-import http from "@/lib/http"
+import http from '@/lib/http'
 
 
 const analytic = {
 const analytic = {
     init() {
     init() {

+ 2 - 2
frontend-next/src/api/auth.ts

@@ -1,5 +1,5 @@
-import http from "@/lib/http"
-import {useUserStore} from "@/pinia/user"
+import http from '@/lib/http'
+import {useUserStore} from '@/pinia/user'
 
 
 const user = useUserStore()
 const user = useUserStore()
 const {login, logout} = user
 const {login, logout} = user

+ 5 - 0
frontend-next/src/api/config.ts

@@ -0,0 +1,5 @@
+import Curd from '@/api/curd'
+
+const config = new Curd('/config')
+
+export default config

+ 34 - 0
frontend-next/src/api/curd.ts

@@ -0,0 +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) {
+        return http.post(this.baseUrl + (id ? '/' + id : ''), data)
+    }
+
+    _destroy(id: any = null) {
+        return http.delete(this.baseUrl + '/' + id)
+    }
+}
+
+export default Curd

+ 32 - 0
frontend-next/src/api/domain.ts

@@ -0,0 +1,32 @@
+import Curd from '@/api/curd'
+import http from '@/lib/http'
+
+class Domain extends Curd {
+    enable(name: string) {
+        return http.post(this.baseUrl + '/' + name + '/enable')
+    }
+
+    disable(name: string) {
+        return http.post(this.baseUrl + '/' + name + '/disable')
+    }
+
+    get_template() {
+        return http.get('template')
+    }
+
+    cert_info(domain: string) {
+        return http.get('cert/' + domain + '/info')
+    }
+
+    add_auto_cert(domain: string) {
+        return http.post('cert/' + domain)
+    }
+
+    remove_auto_cert(domain: string) {
+        return http.delete('cert/' + domain)
+    }
+}
+
+const domain = new Domain('/domain')
+
+export default domain

+ 13 - 0
frontend-next/src/api/ngx.ts

@@ -0,0 +1,13 @@
+import http from '@/lib/http'
+
+const ngx = {
+    build_config(ngxConfig: any) {
+        return http.post('/ngx/build_config', ngxConfig)
+    },
+
+    tokenize_config(content: string) {
+        return http.post('/ngx/tokenize_config', {content})
+    }
+}
+
+export default ngx

+ 5 - 0
frontend-next/src/api/user.ts

@@ -0,0 +1,5 @@
+import Curd from '@/api/curd'
+
+const user: Curd = new Curd('user')
+
+export default user

+ 8 - 11
frontend-next/src/components/Breadcrumb/Breadcrumb.vue

@@ -1,27 +1,24 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import {computed, reactive, ref, watch} from "vue";
-import {useRoute} from "vue-router"
-import {useGettext} from "vue3-gettext";
-
-const {$gettext} = useGettext()
+import {computed, ref} from 'vue'
+import {useRoute} from 'vue-router'
 
 
 interface bread {
 interface bread {
-    name: string
+    name: any
     path: string
     path: string
 }
 }
 
 
-const name = ref('')
+const name = ref()
 const route = useRoute()
 const route = useRoute()
 
 
 const breadList = computed(() => {
 const breadList = computed(() => {
     let _breadList: bread[] = []
     let _breadList: bread[] = []
 
 
-    name.value = (route.name || '').toString()
+    name.value = route.name
 
 
     route.matched.forEach(item => {
     route.matched.forEach(item => {
         //item.name !== 'index' && this.breadList.push(item)
         //item.name !== 'index' && this.breadList.push(item)
         _breadList.push({
         _breadList.push({
-            name: (item.name || '').toString(),
+            name: item.name,
             path: item.path
             path: item.path
         })
         })
     })
     })
@@ -38,9 +35,9 @@ const breadList = computed(() => {
             <router-link
             <router-link
                 v-if="item.name !== name && index !== 1"
                 v-if="item.name !== name && index !== 1"
                 :to="{ path: item.path === '' ? '/' : item.path }"
                 :to="{ path: item.path === '' ? '/' : item.path }"
-            >{{ $gettext(item.name) }}
+            >{{ item.name() }}
             </router-link>
             </router-link>
-            <span v-else>{{ $gettext(item.name) }}</span>
+            <span v-else>{{ item.name() }}</span>
         </a-breadcrumb-item>
         </a-breadcrumb-item>
     </a-breadcrumb>
     </a-breadcrumb>
 </template>
 </template>

+ 4 - 4
frontend-next/src/components/Chart/AreaChart.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import VueApexCharts from 'vue3-apexcharts'
 import VueApexCharts from 'vue3-apexcharts'
-import {ref, watch} from "vue"
-import {useSettingsStore} from "@/pinia/settings"
-import {storeToRefs} from "pinia"
+import {ref, watch} from 'vue'
+import {useSettingsStore} from '@/pinia/settings'
+import {storeToRefs} from 'pinia'
 
 
 const {series, max, y_formatter} = defineProps(['series', 'max', 'y_formatter'])
 const {series, max, y_formatter} = defineProps(['series', 'max', 'y_formatter'])
 
 
@@ -117,7 +117,7 @@ const callback = () => {
                 },
                 },
             }
             }
         }
         }
-    };
+    }
     instance!.updateOptions(chartOptions)
     instance!.updateOptions(chartOptions)
 }
 }
 
 

+ 1 - 2
frontend-next/src/components/Chart/RadialBarChart.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import VueApexCharts from 'vue3-apexcharts'
 import VueApexCharts from 'vue3-apexcharts'
-import app from '@/main'
-import {reactive} from "vue";
+import {reactive} from 'vue'
 
 
 const {series, centerText, colors, name, bottomText}
 const {series, centerText, colors, name, bottomText}
     = defineProps(['series', 'centerText', 'colors', 'name', 'bottomText'])
     = defineProps(['series', 'centerText', 'colors', 'name', 'bottomText'])

+ 19 - 0
frontend-next/src/components/CodeEditor/CodeEditor.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import {VAceEditor} from 'vue3-ace-editor'
+import 'ace-builds/src-noconflict/mode-nginx'
+import 'ace-builds/src-noconflict/theme-monokai'
+
+const {content} = defineProps(['content'])
+</script>
+
+<template>
+    <v-ace-editor
+        v-model:value="content"
+        lang="nginx"
+        theme="monokai"
+        style="height: 300px"/>
+</template>
+
+<style scoped>
+
+</style>

+ 1 - 2
frontend-next/src/components/Logo/Logo.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import logo from '@/assets/img/logo.png'
-</script>
+import logo from '@/assets/img/logo.png'</script>
 
 
 <template>
 <template>
     <div class="logo">
     <div class="logo">

+ 4 - 21
frontend-next/src/components/PageHeader/PageHeader.vue

@@ -1,10 +1,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import Breadcrumb from '@/components/Breadcrumb/Breadcrumb.vue'
 import Breadcrumb from '@/components/Breadcrumb/Breadcrumb.vue'
-import {useRoute} from "vue-router"
-import {computed, ref, watch} from "vue"
-import {useGettext} from "vue3-gettext";
-
-const {$gettext} = useGettext()
+import {useRoute} from 'vue-router'
+import {computed, ref, watch} from 'vue'
 
 
 const {title, logo, avatar} = defineProps(['title', 'logo', 'avatar'])
 const {title, logo, avatar} = defineProps(['title', 'logo', 'avatar'])
 
 
@@ -16,7 +13,7 @@ const display = computed(() => {
 
 
 const name = ref(route.name)
 const name = ref(route.name)
 watch(() => route.name, () => {
 watch(() => route.name, () => {
-    name.value = (route.name || '').toString()
+    name.value = route.name
 })
 })
 
 
 </script>
 </script>
@@ -30,26 +27,12 @@ watch(() => route.name, () => {
                     <div class="row">
                     <div class="row">
                         <img v-if="logo" :src="logo" class="logo"/>
                         <img v-if="logo" :src="logo" class="logo"/>
                         <h1 class="title">
                         <h1 class="title">
-                            {{ $gettext(name.toString()) }}
+                            {{ name() }}
                         </h1>
                         </h1>
                         <div class="action">
                         <div class="action">
                             <slot name="action"></slot>
                             <slot name="action"></slot>
                         </div>
                         </div>
                     </div>
                     </div>
-                    <div class="row">
-                        <div v-if="avatar" class="avatar">
-                            <a-avatar :src="avatar"/>
-                        </div>
-                        <div v-if="this.$slots.content" class="headerContent">
-                            <slot name="content"></slot>
-                        </div>
-                        <div v-if="this.$slots.extra" class="extra">
-                            <slot name="extra"></slot>
-                        </div>
-                    </div>
-                    <div>
-                        <slot name="pageMenu"></slot>
-                    </div>
                 </div>
                 </div>
             </div>
             </div>
         </div>
         </div>

+ 0 - 3
frontend-next/src/components/PageHeader/index.js

@@ -1,3 +0,0 @@
-import PageHeader from './PageHeader'
-
-export default PageHeader

+ 8 - 6
frontend-next/src/components/SetLanguage/SetLanguage.vue

@@ -1,12 +1,15 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import gettext from "@/gettext"
+import gettext from '@/gettext'
 
 
 
 
-import {ref, watch, nextTick} from "vue"
+import {ref, watch} from 'vue'
+
+import {useSettingsStore} from '@/pinia/settings'
+import {useRoute} from 'vue-router'
 
 
-import {useSettingsStore} from "@/pinia/settings"
 const settings = useSettingsStore()
 const settings = useSettingsStore()
 
 
+const route = useRoute()
 
 
 const current = ref(gettext.current)
 const current = ref(gettext.current)
 
 
@@ -14,9 +17,8 @@ const languageAvailable = gettext.available
 watch(current, (v) => {
 watch(current, (v) => {
     settings.set_language(v)
     settings.set_language(v)
     gettext.current = v
     gettext.current = v
-    // nextTick(() => {
-    //     location.reload()
-    // })
+    // @ts-ignored
+    document.title = route.name() + ' | Nginx UI'
 })
 })
 
 
 </script>
 </script>

+ 130 - 190
frontend-next/src/components/StdDataDisplay/StdCurd.vue

@@ -1,7 +1,122 @@
+<script lang="ts">
+import gettext from '@/gettext'
+
+const {$gettext, interpolate} = gettext
+</script>
+<script setup lang="ts">
+import StdTable from './StdTable.vue'
+// import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
+
+import {reactive, ref} from 'vue'
+import {message} from 'ant-design-vue'
+
+const props = defineProps({
+    api: Object,
+    columns: Array,
+    title: {
+        type: String,
+        default: $gettext('Table')
+    },
+    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
+    },
+})
+
+const visible = ref(false)
+const update = ref(0)
+let data = reactive({id: null})
+let error = reactive({})
+const params = reactive({})
+const selected = reactive([])
+
+function onSelect(keys: any) {
+    selected.concat(...keys)
+}
+
+function editableColumns() {
+    return props.columns!.filter((c: any) => {
+        return c.edit
+    })
+}
+
+function add() {
+    data = reactive({
+        id: null
+    })
+    visible.value = true
+}
+
+const table = ref(null)
+
+interface Table {
+    get_list(): void
+}
+
+const ok = async () => {
+    error = reactive({})
+    props.api!.save(data.id, data).then((r: any) => {
+        message.success($gettext('Save Successfully'))
+        Object.assign(data, r)
+        const t: Table | null = table.value
+        t!.get_list()
+
+    }).catch((e: any) => {
+        message.error((e?.message ?? $gettext('Server error')), 5)
+        error = e.errors
+    })
+}
+
+function cancel() {
+    visible.value = false
+    error = reactive({})
+}
+
+function edit(id: any) {
+    props.api!.get(id).then((r: any) => {
+        Object.assign(data, r)
+        visible.value = true
+    }).catch((e: any) => {
+        message.error((e?.message ?? $gettext('Server error')), 5)
+    })
+}
+
+</script>
+
 <template>
 <template>
     <div class="std-curd">
     <div class="std-curd">
         <a-card :title="title">
         <a-card :title="title">
-            <a v-if="!disable_add" slot="extra" @click="add">添加</a>
+            <template v-if="!disable_add" #extra>
+                <a @click="add" v-translate>Add</a>
+            </template>
+
             <std-table
             <std-table
                 ref="table"
                 ref="table"
                 v-bind="this.$props"
                 v-bind="this.$props"
@@ -14,207 +129,32 @@
                 </template>
                 </template>
             </std-table>
             </std-table>
         </a-card>
         </a-card>
+
         <a-modal
         <a-modal
             class="std-curd-edit-modal"
             class="std-curd-edit-modal"
             :mask="false"
             :mask="false"
-            :title="data.id ? '编辑 ID: ' + data.id : '添加'"
+            :title="data.id ? $gettext('Modify') : $gettext('Add')"
             :visible="visible"
             :visible="visible"
-            cancel-text="关闭"
-            ok-text="保存"
-            @cancel="visible=false;error={}"
+            :cancel-text="$gettext('Cancel')"
+            :ok-text="$gettext('OK')"
+            @cancel="cancel"
             @ok="ok"
             @ok="ok"
             :width="600"
             :width="600"
             destroyOnClose
             destroyOnClose
         >
         >
-            <std-data-entry ref="std_data_entry" :data-list="editableColumns()" :data-source="data"
-                            :error="error">
-                <div slot="supplement">
-                    <slot name="supplement"></slot>
-                </div>
-                <div slot="action">
-                    <slot name="action"></slot>
-                </div>
-            </std-data-entry>
-        </a-modal>
-        <footer-tool-bar v-if="batch_columns.length">
-            <a-space>
-                当前已选中{{ selected.length }}条数据
-                <a-button :disabled="!selected.length"
-                          @click="selected=[];update++">清空选中
-                </a-button>
-                <a-button type="primary"
-                          :disabled="!selected.length"
-                          @click="visible_batch_edit=true" ghost>批量修改
-                </a-button>
-            </a-space>
-        </footer-tool-bar>
-        <a-modal
-            :mask="false"
-            title="批量修改"
-            :visible="visible_batch_edit"
-            cancel-text="取消"
-            ok-text="保存"
-            @cancel="visible_batch_edit=false"
-            @ok="okBatchEdit"
-        >
-            留空则不修改
-            <std-data-entry :data-list="batch_columns" :data-source="data"/>
+            <!--            <std-data-entry ref="std_data_entry" :data-list="editableColumns()" :data-source="data"-->
+            <!--                            :error="error">-->
+            <!--                <div slot="supplement">-->
+            <!--                    <slot name="supplement"></slot>-->
+            <!--                </div>-->
+            <!--                <div slot="action">-->
+            <!--                    <slot name="action"></slot>-->
+            <!--                </div>-->
+            <!--            </std-data-entry>-->
         </a-modal>
         </a-modal>
     </div>
     </div>
 </template>
 </template>
 
 
-<script>
-import StdTable from './StdTable'
-import StdDataEntry from '@/components/StdDataEntry/StdDataEntry'
-import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
-
-export default {
-    name: 'StdCurd',
-    components: {
-        StdTable,
-        StdDataEntry,
-        FooterToolBar
-    },
-    props: {
-        api: Object,
-        columns: Array,
-        title: {
-            type: String,
-            default: '列表'
-        },
-        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
-        },
-    },
-    data() {
-        return {
-            visible: false,
-            visible_batch_edit: false,
-            data: {
-                id: null,
-            },
-            error: {},
-            params: {},
-            selected: [],
-            batch_columns: this.batchColumns(),
-            update: 0,
-        }
-    },
-    methods: {
-        onSelect(keys) {
-            this.selected = keys
-        },
-        batchColumns() {
-            return this.columns.filter((column) => {
-                return column.batch
-                    && column.edit && column.edit.type !== 'upload'
-                    && column.edit.type !== 'transfer'
-            })
-        },
-        okBatchEdit() {
-            this.api.batchSave(this.selected, this.data)
-                .then(() => {
-                    this.$message.success('批量修改成功')
-                    this.$refs.table.get_list()
-                }).catch(e => {
-                this.$message.error(e.message)
-            })
-        },
-        editableColumns() {
-            return this.columns.filter((c) => {
-                return c.edit
-            })
-        },
-        uploadColumns() {
-            return this.columns.filter(c => {
-                return c.edit && c.edit.type === 'upload'
-            })
-        },
-        async add() {
-            this.data = {
-                id: null
-            }
-            this.visible = true
-        },
-        async do_upload() {
-            const columns = await this.uploadColumns()
-
-            for (let i = 0; i < columns.length; i++) {
-                const refs = this.$refs.std_data_entry.$refs
-                const t = refs['std_upload_' + columns[i].dataIndex][0]
-                if (t) {
-                    await t.upload()
-                }
-            }
-        },
-        async ok() {
-            this.error = {}
-            if (this.data.id) {
-                await this.do_upload()
-                this.api.save((this.data.id ? this.data.id : null), this.data).then(r => {
-                    this.$message.success('保存成功')
-                    this.data = Object.assign(this.data, r)
-                    this.$refs.table.get_list()
-                }).catch(error => {
-                    this.$message.error((error.message ? error.message : '保存失败'), 5)
-                    this.error = error.errors
-                })
-
-            } else {
-                this.api.save((this.data.id ? this.data.id : null), this.data).then(r => {
-                    this.$message.success('保存成功')
-                    this.data = this.extend(this.data, r)
-                    this.$nextTick().then(() => {
-                        this.do_upload()
-                    })
-                    this.$refs.table.get_list()
-                }).catch(error => {
-                    this.$message.error((error.message ? error.message : '保存失败'), 5)
-                    this.error = error.errors
-                })
-            }
-        },
-        edit(id) {
-            this.api.get(id).then(r => {
-                this.data = r
-                this.visible = true
-            }).catch(e => {
-                console.log(e)
-                this.$message.error('系统错误')
-            })
-        }
-    }
-}
-</script>
-
 <style lang="less" scoped>
 <style lang="less" scoped>
 
 
 </style>
 </style>

+ 239 - 328
frontend-next/src/components/StdDataDisplay/StdTable.vue

@@ -1,28 +1,227 @@
+<script setup lang="ts">
+import gettext from '@/gettext'
+
+const {$gettext, interpolate} = gettext
+
+import StdPagination from './StdPagination.vue'
+import {nextTick, reactive, ref} from 'vue'
+import {useRoute} from 'vue-router'
+import {message} from 'ant-design-vue'
+
+const props = defineProps({
+    api: Object,
+    columns: Array,
+    data_key: {
+        type: String,
+        default: 'data'
+    },
+    disable_search: {
+        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,
+        default: 'checkbox',
+        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'
+    }
+})
+
+
+const data_source = reactive([])
+const loading = ref(true)
+const pagination = ({
+    total: 1,
+    per_page: 10,
+    current_page: 1,
+    total_pages: 1
+})
+const route = useRoute()
+const params = reactive({
+    ...route.query,
+    ...props.get_params
+})
+let selectedRowKeys = ref([])
+const rowSelection = reactive({})
+
+const searchColumns = getSearchColumns()
+const pithyColumns = getPithyColumns()
+
+
+get_list()
+
+defineExpose({
+    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(e?.message ?? $gettext('Server error'))
+    })
+}
+
+function get_list(page_num = null) {
+    loading.value = true
+    if (page_num) {
+        params['page'] = page_num
+    }
+    props.api!.get_list(params).then((r: any) => {
+        Object.assign(data_source, r.data)
+
+        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) {
+        params['order_by'] = sorter.field
+        params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
+        nextTick(() => {
+            get_list()
+        })
+    }
+}
+
+function getSearchColumns() {
+    let searchColumns: any = []
+    props.columns!.forEach((column: any) => {
+        if (column.search) {
+            if (column.edit && column.edit.type !== 'upload'
+                && column.edit.type !== 'transfer') {
+                const tmp = Object.assign({}, column)
+                tmp.edit = Object.assign({}, column.edit)
+                if (typeof column.search === 'string') {
+                    tmp.edit.type = column.search
+                } else if (typeof column.search === 'object') {
+                    tmp.edit = column.search
+                }
+                searchColumns.push(tmp)
+            }
+            // search 覆盖 edit
+            if (!column.edit) {
+                const tmp = Object.assign({}, column)
+                tmp.edit = Object.assign({}, column.edit)
+                if (typeof column.search === 'object') {
+                    tmp.edit = column.search
+                }
+                searchColumns.push(tmp)
+            }
+        }
+    })
+    return searchColumns
+}
+
+function getPithyColumns() {
+    if (props.pithy) {
+        return props.columns!.filter((c: any, index: any, columns: any) => {
+            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
+}
+
+function onSelectChange(_selectedRowKeys: any) {
+    selectedRowKeys = reactive(_selectedRowKeys)
+    // this.$emit('selected', selectedRowKeys)
+}
+
+function onSelect(record) {
+    // this.$emit('selectedRecord', record)
+}
+
+function handleClick(data, index, method = '', path = '') {
+    if (method === 'router') {
+        this.$router.push(path + '/' + data).then()
+    } else {
+        this.params[index] = data
+        this.get_list()
+    }
+}
+
+function row(record) {
+    return {
+        on: {
+            click: () => {
+                this.$emit('clickRow', record.id)
+            }
+        }
+    }
+}
+
+const reset_search = async () => {
+    this.params = {}
+    await this.$router.push({query: {}}).catch(() => {
+    })
+}
+</script>
+
 <template>
 <template>
     <div class="std-table">
     <div class="std-table">
-        <std-data-entry
-            v-if="!disable_search"
-            :data-list="searchColumns"
-            v-model="params"
-            layout="inline"
-        >
-            <div slot="action">
-                <a-form-item :wrapper-col="{span:8}">
-                    <a-button type="primary" @click="$router.push({
-                        query: Object.assign({}, params),
-                    }).catch(() => {})">查询
-                    </a-button>
-                </a-form-item>
-                <a-form-item :wrapper-col="{span:8}">
-                    <a-button @click="reset_search">重置</a-button>
-                </a-form-item>
-            </div>
-        </std-data-entry>
-        <div v-if="soft_delete" style="text-align: right">
-            <a v-if="params['trashed']" href="javascript:;"
-               @click="params['trashed']=false; get_list()">返回</a>
-            <a v-else href="javascript:;" @click="params['trashed']=true; get_list()">回收站</a>
-        </div>
+        <!--        <std-data-entry-->
+        <!--            v-if="!disable_search"-->
+        <!--            :data-list="searchColumns"-->
+        <!--            v-model="params"-->
+        <!--            layout="inline"-->
+        <!--        >-->
+        <!--            <div slot="action">-->
+        <!--                <a-form-item :wrapper-col="{span:8}">-->
+        <!--                    <a-button type="primary" @click="$router.push({-->
+        <!--                        query: Object.assign({}, params),-->
+        <!--                    }).catch(() => {})">查询-->
+        <!--                    </a-button>-->
+        <!--                </a-form-item>-->
+        <!--                <a-form-item :wrapper-col="{span:8}">-->
+        <!--                    <a-button @click="reset_search">重置</a-button>-->
+        <!--                </a-form-item>-->
+        <!--            </div>-->
+        <!--        </std-data-entry>-->
         <a-table
         <a-table
             :columns="pithyColumns"
             :columns="pithyColumns"
             :customRow="row"
             :customRow="row"
@@ -36,319 +235,31 @@
             :scroll="{ x: scrollX }"
             :scroll="{ x: scrollX }"
         >
         >
             <template
             <template
-                v-for="c in pithyColumns"
-                :slot="c.scopedSlots.customRender"
-                slot-scope="text, record"
+                v-slot:bodyCell="{text, record, index, column}"
             >
             >
-                <div v-if="c.badge" :key="c.dataIndex">
-                    <a-badge v-if="text === true || text > 0" status="success"/>
-                    <a-badge v-else status="error"/>
-                    {{ c.mask ? c.mask[text] : text }}
-                </div>
-                <span v-else-if="c.datetime"
-                      :key="c.dataIndex">{{ text ? moment(text).format('yyyy-MM-DD HH:mm:ss') : '无' }}</span>
-                <span v-else-if="c.date" :key="c.dataIndex">{{ text ? moment(text).format('yyyy-MM-DD') : '无' }}</span>
-                <div v-else-if="c.click" :key="c.dataIndex">
-                    <a href="javascript:;"
-                       @click="handleClick(
-                           record[c.click.index?c.click.index:c.dataIndex],
-                           c.click.index?c.click.index:c.dataIndex,
-                           c.click.method, c.click.path)">
-                        {{ text != null ? text : c.default }}
+                <template v-if="column.dataIndex === 'action'">
+                    <a v-if="props.editable" @click="$emit('clickEdit', record[props.rowKey], record)">
+                        {{ props.edit_text || $gettext('Modify') }}
                     </a>
                     </a>
-                </div>
-                <span v-else :key="c.dataIndex">{{ text != null ? (c.mask ? c.mask[text] : text) : c.default }}</span>
-            </template>
-            <div class="std_action" v-if="!pithy" slot="action" slot-scope="text, record">
-                <a v-if="editable" @click="$emit('clickEdit', record[rowKey], record)">
-                    {{ edit_text }}
-                </a>
-                <slot name="actions" :record="record"/>
-                <template v-if="deletable">
-                    <a-divider type="vertical"/>
-                    <a-popconfirm
-                        v-if="soft_delete&&params.trashed"
-                        :cancelText="cancel_text"
-                        :okText="ok_text"
-                        :title="restore_title_text"
-                        @confirm="restore(record[rowKey])">
-                        <a href="javascript:;">{{ restore_action_text }}</a>
-                    </a-popconfirm>
-                    <a-popconfirm
-                        v-else
-                        :cancelText="cancel_text"
-                        :okText="ok_text"
-                        :title="destroy_title_text"
-                        @confirm="destroy(record[rowKey])">
-                        <a href="javascript:;">{{ destroy_action_text }}</a>
-                    </a-popconfirm>
+                    <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 v-translate>Delete</a>
+                        </a-popconfirm>
+                    </template>
                 </template>
                 </template>
-            </div>
+            </template>
+
         </a-table>
         </a-table>
         <std-pagination :pagination="pagination" @changePage="get_list"/>
         <std-pagination :pagination="pagination" @changePage="get_list"/>
     </div>
     </div>
 </template>
 </template>
 
 
-<script lang="ts">
-import StdPagination from './StdPagination.vue'
-import moment from 'moment'
-import StdDataEntry from '@/components/StdDataEntry/StdDataEntry.vue'
-import gettext from '@/gettext'
-const {$gettext, interpolate} = gettext
-
-export default {
-    name: 'StdTable',
-    components: {
-        StdDataEntry,
-        StdPagination,
-    },
-    props: {
-        columns: Array,
-        api: Object,
-        data_key: String,
-        selectionType: {
-            type: String,
-            default: 'checkbox',
-            validator: function (value) {
-                return ['checkbox', 'radio'].indexOf(value) !== -1
-            }
-        },
-        pithy: {
-            type: Boolean,
-            default: false
-        },
-        disable_search: {
-            type: Boolean,
-            default: false
-        },
-        soft_delete: {
-            type: Boolean,
-            default: false
-        },
-        edit_text: {
-            type: String,
-            default() {
-                return this.$gettext('Edit')
-            }
-        },
-        restore_title_text: {
-            type: String,
-            default() {
-                return this.$gettext('Are you sure you want to restore?')
-            }
-        },
-        restore_action_text: {
-            type: String,
-            default() {
-                return this.$gettext('Restore')
-            }
-        },
-        ok_text: {
-            type: String,
-            default() {
-                return this.$gettext('Yes, I\'m sure')
-            }
-        },
-        cancel_text: {
-            type: String,
-            default() {
-                return this.$gettext('No, I\'m rethink')
-            }
-        },
-        destroy_title_text: {
-            type: String,
-            default() {
-                return this.$gettext('Are you sure you want to destroy?')
-            }
-        },
-        destroy_action_text: {
-            type: String,
-            default() {
-                return this.$gettext('Destroy')
-            }
-        },
-        deletable: {
-            type: Boolean,
-            default: true
-        },
-        editable: {
-            type: Boolean,
-            default: true
-        },
-        get_params: {
-            type: Object,
-            default() {
-                return {}
-            }
-        },
-        scrollX: {
-            type: [Number, Boolean],
-            default: true
-        },
-        rowKey: {
-            type: String,
-            default: 'id'
-        }
-    },
-    data() {
-        return {
-            moment,
-            data_source: [],
-            loading: true,
-            pagination: {
-                total: 1,
-                per_page: 10,
-                current_page: 1,
-                total_pages: 1
-            },
-            params: {
-                ...this.$route.query,
-                ...this.get_params
-            },
-            selectedRowKeys: [],
-            rowSelection: {},
-            searchColumns: this.get_searchColumns(),
-            pithyColumns: this.get_pithyColumns(),
-        }
-    },
-    watch: {
-        $route() {
-            this.get_list()
-        }
-    },
-    created() {
-        this.get_list()
-    },
-    methods: {
-        restore(id) {
-            this.api.restore(id).then(() => {
-                this.get_list()
-                this.$message.success('反删除 ID: ' + id + ' 成功')
-            }).catch(e => {
-                console.log(e)
-                this.$message.error(e?.message ?? '系统错误')
-            })
-        },
-        destroy(id) {
-            this.api.destroy(id).then(() => {
-                this.get_list()
-                this.$message.success($interpolate($gettext('Delete ID: %{id}'), {id: id}))
-            }).catch(e => {
-                console.log(e)
-                this.$message.error(e?.message ?? $gettext('Server error'))
-            })
-        },
-        get_list(page_num = null) {
-            this.loading = true
-            if (page_num) {
-                this.params['page'] = page_num
-            }
-            this.api.get_list(this.params).then(response => {
-                if (response[this.data_key] === undefined && response.data !== undefined) {
-                    this.data_source = response.data
-                } else {
-                    this.data_source = response[this.data_key]
-                }
-                if (response.pagination !== undefined) {
-                    this.pagination = response.pagination
-                }
-                this.loading = false
-            }).catch(e => {
-                console.log(e)
-                this.$message.error(e?.message ?? '系统错误')
-            })
-        },
-        stdChange(pagination, filters, sorter) {
-            if (sorter) {
-                this.params['order_by'] = sorter.field
-                this.params['sort'] = sorter.order === 'ascend' ? 'asc' : 'desc'
-                this.$nextTick(() => {
-                    this.get_list()
-                })
-            }
-        },
-        get_searchColumns() {
-            let searchColumns = []
-            this.columns.forEach(column => {
-                if (column.search) {
-                    if (column.edit && column.edit.type !== 'upload'
-                        && column.edit.type !== 'transfer') {
-                        const tmp = Object.assign({}, column)
-                        tmp.edit = Object.assign({}, column.edit)
-                        if (typeof column.search === 'string') {
-                            tmp.edit.type = column.search
-                        } else if (typeof column.search === 'object') {
-                            tmp.edit = column.search
-                        }
-                        searchColumns.push(tmp)
-                    }
-                    // search 覆盖 edit
-                    if (!column.edit) {
-                        const tmp = Object.assign({}, column)
-                        tmp.edit = Object.assign({}, column.edit)
-                        if (typeof column.search === 'object') {
-                            tmp.edit = column.search
-                        }
-                        searchColumns.push(tmp)
-                    }
-                }
-            })
-            return searchColumns
-        },
-        get_pithyColumns() {
-            if (this.pithy) {
-                return this.columns.filter((c, index, columns) => {
-                    let display = c.pithy === true && c.display !== false
-                    columns[index]['scopedSlots'] = {}
-                    columns[index]['scopedSlots']['customRender'] =
-                        c.dataIndex !== 'title' ? c.dataIndex : '_' + c.dataIndex
-                    return display
-                })
-            }
-            return this.columns.filter((c, index, columns) => {
-                let display = c.display !== false
-                columns[index]['scopedSlots'] = {}
-                columns[index]['scopedSlots']['customRender'] =
-                    c.dataIndex !== 'title' ? c.dataIndex : '_' + c.dataIndex
-                return display
-            })
-        },
-        checked(c) {
-            this.params[c.target.value] = c.target.checked
-        },
-        onSelectChange(selectedRowKeys) {
-            this.selectedRowKeys = selectedRowKeys
-            this.$emit('selected', selectedRowKeys)
-        },
-        onSelect(record) {
-            this.$emit('selectedRecord', record)
-        },
-        handleClick(data, index, method = '', path = '') {
-            if (method === 'router') {
-                this.$router.push(path + '/' + data).then()
-            } else {
-                this.params[index] = data
-                this.get_list()
-            }
-        },
-        row(record) {
-            return {
-                on: {
-                    click: () => {
-                        this.$emit('clickRow', record.id)
-                    }
-                }
-            }
-        },
-        async reset_search() {
-            this.params = {}
-            await this.$router.push({query: {}}).catch(() => {
-            })
-        }
-    }
-}
-</script>
-
 <style lang="less">
 <style lang="less">
 .ant-table-scroll {
 .ant-table-scroll {
     .ant-table-body {
     .ant-table-body {

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

@@ -0,0 +1,17 @@
+// text, record, index, column
+import dayjs from 'dayjs'
+
+export interface customRender {
+    text: any
+    record: any
+    index: any
+    column: any
+}
+
+export const datetime = (args: customRender) => {
+    return dayjs(args.text).format('YYYY-MM-DD HH:mm:ss')
+}
+
+export const date = (args: customRender) => {
+    return dayjs(args.text).format('YYYY-MM-DD')
+}

+ 0 - 0
frontend-next/src/api/login.ts → frontend-next/src/components/StdDataDisplay/index.ts


+ 5 - 5
frontend-next/src/gettext.ts

@@ -1,13 +1,13 @@
-import {createGettext} from "vue3-gettext"
+import {createGettext} from 'vue3-gettext'
 import translations from './language/translations.json'
 import translations from './language/translations.json'
 
 
 export default createGettext({
 export default createGettext({
     availableLanguages: {
     availableLanguages: {
-        en: "En",
-        zh_CN: "简",
-        zh_TW: "繁",
+        en: 'En',
+        zh_CN: '简',
+        zh_TW: '繁',
     },
     },
-    defaultLanguage: "en",
+    defaultLanguage: 'en',
     translations: translations,
     translations: translations,
     silent: true
     silent: true
 })
 })

File diff suppressed because it is too large
+ 0 - 0
frontend-next/src/language/translations.json


+ 11 - 6
frontend-next/src/layouts/HeaderLayout.vue

@@ -1,19 +1,20 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import gettext from '@/gettext'
 import gettext from '@/gettext'
-import {message} from "ant-design-vue"
+import {message} from 'ant-design-vue'
 import auth from '@/api/auth'
 import auth from '@/api/auth'
-// import {HomeOutlined, LogoutOutlined} from "@ant-design/icons-vue"
+import {HomeOutlined, LogoutOutlined, MenuUnfoldOutlined} from '@ant-design/icons-vue'
+import {useRouter} from 'vue-router'
 
 
 const {$gettext} = gettext
 const {$gettext} = gettext
-import {useRouter} from "vue-router"
 
 
 const router = useRouter()
 const router = useRouter()
 
 
 function logout() {
 function logout() {
     auth.logout().then(() => {
     auth.logout().then(() => {
         message.success($gettext('Logout successful'))
         message.success($gettext('Logout successful'))
-        window.location.reload()
+    }).then(() => {
+        router.push('/login')
     })
     })
 }
 }
 
 
@@ -21,15 +22,19 @@ function logout() {
 
 
 <template>
 <template>
     <div class="header">
     <div class="header">
+        <div class="tool">
+            <MenuUnfoldOutlined @click="$emit('clickUnFold')"/>
+        </div>
+
         <div class="user-wrapper">
         <div class="user-wrapper">
             <set-language class="set_lang"/>
             <set-language class="set_lang"/>
 
 
             <a href="/">
             <a href="/">
-                <!--                <HomeOutlined/>-->
+                <HomeOutlined/>
             </a>
             </a>
 
 
             <a @click="logout" style="margin-left: 20px">
             <a @click="logout" style="margin-left: 20px">
-                <!--                <LogoutOutlined/>-->
+                <LogoutOutlined/>
             </a>
             </a>
         </div>
         </div>
     </div>
     </div>

+ 7 - 10
frontend-next/src/layouts/SideBar.vue

@@ -1,11 +1,8 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import Logo from '@/components/Logo/Logo.vue'
 import Logo from '@/components/Logo/Logo.vue'
 import {routes} from '@/routes'
 import {routes} from '@/routes'
-import {useRoute} from "vue-router"
-import {computed, ref, watch} from "vue"
-import {useGettext} from "vue3-gettext"
-
-const {$gettext} = useGettext()
+import {useRoute} from 'vue-router'
+import {computed, ref, watch} from 'vue'
 
 
 const route = useRoute()
 const route = useRoute()
 
 
@@ -37,7 +34,7 @@ interface meta {
 
 
 interface sidebar {
 interface sidebar {
     path: string
     path: string
-    name: string
+    name: Function
     meta: meta,
     meta: meta,
     children: sidebar[]
     children: sidebar[]
 }
 }
@@ -57,7 +54,7 @@ const visible = computed(() => {
             children: []
             children: []
         };
         };
 
 
-        (s.children || []).forEach(c => {
+        (s.children || []).forEach((c: any) => {
             if (c.meta && c.meta.hiddenInSidebar) {
             if (c.meta && c.meta.hiddenInSidebar) {
                 return
                 return
             }
             }
@@ -85,17 +82,17 @@ const visible = computed(() => {
                              :key="sidebar.name"
                              :key="sidebar.name"
                              @click="$router.push('/'+sidebar.path).catch(() => {})">
                              @click="$router.push('/'+sidebar.path).catch(() => {})">
                     <component :is="sidebar.meta.icon"/>
                     <component :is="sidebar.meta.icon"/>
-                    <span>{{ $gettext(sidebar.name) }}</span>
+                    <span>{{ sidebar.name() }}</span>
                 </a-menu-item>
                 </a-menu-item>
 
 
                 <a-sub-menu v-else :key="sidebar.path">
                 <a-sub-menu v-else :key="sidebar.path">
                     <template #title>
                     <template #title>
                         <component :is="sidebar.meta.icon"/>
                         <component :is="sidebar.meta.icon"/>
-                        <span>{{ $gettext(sidebar.name) }}</span>
+                        <span>{{ sidebar.name() }}</span>
                     </template>
                     </template>
                     <a-menu-item v-for="child in sidebar.children" :key="child.name">
                     <a-menu-item v-for="child in sidebar.children" :key="child.name">
                         <router-link :to="'/'+sidebar.path+'/'+child.path">
                         <router-link :to="'/'+sidebar.path+'/'+child.path">
-                            {{ $gettext(child.name) }}
+                            {{ child.name() }}
                         </router-link>
                         </router-link>
                     </a-menu-item>
                     </a-menu-item>
                 </a-sub-menu>
                 </a-sub-menu>

+ 2 - 2
frontend-next/src/lib/http/index.ts

@@ -1,6 +1,6 @@
 import axios, {AxiosRequestConfig} from 'axios'
 import axios, {AxiosRequestConfig} from 'axios'
-import {useUserStore} from "@/pinia/user"
-import {storeToRefs} from "pinia";
+import {useUserStore} from '@/pinia/user'
+import {storeToRefs} from 'pinia'
 
 
 const user = useUserStore()
 const user = useUserStore()
 
 

+ 15 - 15
frontend-next/src/lib/theme/index.ts

@@ -5,27 +5,27 @@ function changeCss(css: string, value: string) {
 
 
 function changeTheme(theme: string) {
 function changeTheme(theme: string) {
     const head = document.head
     const head = document.head
-    document.getElementById("theme")?.remove()
-    const styleDom = document.createElement("style")
-    styleDom.id = "theme"
+    document.getElementById('theme')?.remove()
+    const styleDom = document.createElement('style')
+    styleDom.id = 'theme'
     styleDom.innerHTML = theme
     styleDom.innerHTML = theme
     head.appendChild(styleDom)
     head.appendChild(styleDom)
 }
 }
 
 
 export const dark_mode = async (enabled: Boolean) => {
 export const dark_mode = async (enabled: Boolean) => {
     if (enabled) {
     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)")
+        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 {
     } 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)")
+        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)')
     }
     }
 }
 }

+ 3 - 3
frontend-next/src/lib/websocket/index.ts

@@ -1,6 +1,6 @@
-import ReconnectingWebSocket from "reconnecting-websocket"
-import {useUserStore} from "@/pinia/user"
-import {storeToRefs} from "pinia"
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import {useUserStore} from '@/pinia/user'
+import {storeToRefs} from 'pinia'
 
 
 
 
 function ws(url: string): ReconnectingWebSocket {
 function ws(url: string): ReconnectingWebSocket {

+ 4 - 4
frontend-next/src/main.ts

@@ -1,11 +1,11 @@
 import {createApp} from 'vue'
 import {createApp} from 'vue'
-import {createPinia} from "pinia"
-import gettext from "./gettext"
+import {createPinia} from 'pinia'
+import gettext from './gettext'
 import App from './App.vue'
 import App from './App.vue'
-import router from "./routes"
+import router from './routes'
 import 'ant-design-vue/dist/antd.less'
 import 'ant-design-vue/dist/antd.less'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
-import {useSettingsStore} from "@/pinia/settings"
+import {useSettingsStore} from '@/pinia/settings'
 
 
 
 
 const pinia = createPinia()
 const pinia = createPinia()

+ 1 - 1
frontend-next/src/pinia/settings.ts

@@ -1,4 +1,4 @@
-import {defineStore} from "pinia"
+import {defineStore} from 'pinia'
 
 
 export const useSettingsStore = defineStore('settings', {
 export const useSettingsStore = defineStore('settings', {
     state: () => ({
     state: () => ({

+ 1 - 1
frontend-next/src/pinia/user.ts

@@ -1,4 +1,4 @@
-import {defineStore} from "pinia"
+import {defineStore} from 'pinia'
 
 
 export const useUserStore = defineStore('user', {
 export const useUserStore = defineStore('user', {
     state: () => ({
     state: () => ({

+ 38 - 38
frontend-next/src/routes/index.ts

@@ -1,47 +1,45 @@
-import {createRouter, createWebHistory} from "vue-router"
-import gettext from "../gettext"
-
-const {$gettext} = gettext
-
-import {useUserStore} from "@/pinia/user"
+import {createRouter, createWebHistory} from 'vue-router'
+import gettext from '../gettext'
+import {useUserStore} from '@/pinia/user'
 
 
 import {
 import {
-    HomeOutlined,
-    UserOutlined,
     CloudOutlined,
     CloudOutlined,
-    FileOutlined,
     CodeOutlined,
     CodeOutlined,
-    InfoCircleOutlined
+    FileOutlined,
+    HomeOutlined,
+    InfoCircleOutlined,
+    UserOutlined
+} from '@ant-design/icons-vue'
 
 
-} from "@ant-design/icons-vue"
+const {$gettext} = gettext
 
 
 export const routes = [
 export const routes = [
     {
     {
         path: '/',
         path: '/',
-        name: $gettext('Home'),
+        name: () => $gettext('Home'),
         component: () => import('@/layouts/BaseLayout.vue'),
         component: () => import('@/layouts/BaseLayout.vue'),
         redirect: '/dashboard',
         redirect: '/dashboard',
         children: [
         children: [
             {
             {
                 path: 'dashboard',
                 path: 'dashboard',
                 component: () => import('@/views/dashboard/DashBoard.vue'),
                 component: () => import('@/views/dashboard/DashBoard.vue'),
-                name: $gettext('Dashboard'),
+                name: () => $gettext('Dashboard'),
                 meta: {
                 meta: {
-                    hiddenHeaderContent: true,
+                    // hiddenHeaderContent: true,
                     icon: HomeOutlined
                     icon: HomeOutlined
                 }
                 }
             },
             },
             {
             {
                 path: 'user',
                 path: 'user',
-                name: $gettext('Manage Users'),
-                // component: () => import('@/views/user/User.vue'),
+                name: () => $gettext('Manage Users'),
+                component: () => import('@/views/user/User.vue'),
                 meta: {
                 meta: {
                     icon: UserOutlined
                     icon: UserOutlined
                 },
                 },
             },
             },
             {
             {
                 path: 'domain',
                 path: 'domain',
-                name: $gettext('Manage Sites'),
+                name: () => $gettext('Manage Sites'),
                 component: () => import('@/layouts/BaseRouterView.vue'),
                 component: () => import('@/layouts/BaseRouterView.vue'),
                 meta: {
                 meta: {
                     icon: CloudOutlined
                     icon: CloudOutlined
@@ -49,16 +47,16 @@ export const routes = [
                 redirect: '/domain/list',
                 redirect: '/domain/list',
                 children: [{
                 children: [{
                     path: 'list',
                     path: 'list',
-                    name: $gettext('Sites List'),
-                    // component: () => import('@/views/domain/DomainList.vue'),
+                    name: () => $gettext('Sites List'),
+                    component: () => import('@/views/domain/DomainList.vue'),
                 }, {
                 }, {
                     path: 'add',
                     path: 'add',
-                    name: $gettext('Add Site'),
+                    name: () => $gettext('Add Site'),
                     // component: () => import('@/views/domain/DomainAdd.vue'),
                     // component: () => import('@/views/domain/DomainAdd.vue'),
                 }, {
                 }, {
                     path: ':name',
                     path: ':name',
-                    name: $gettext('Edit Site'),
-                    // component: () => import('@/views/domain/DomainEdit.vue'),
+                    name: () => $gettext('Edit Site'),
+                    component: () => import('@/views/domain/DomainEdit.vue'),
                     meta: {
                     meta: {
                         hiddenInSidebar: true
                         hiddenInSidebar: true
                     }
                     }
@@ -66,8 +64,8 @@ export const routes = [
             },
             },
             {
             {
                 path: 'config',
                 path: 'config',
-                name: $gettext('Manage Configs'),
-                // component: () => import('@/views/config/Config.vue'),
+                name: () => $gettext('Manage Configs'),
+                component: () => import('@/views/config/Config.vue'),
                 meta: {
                 meta: {
                     icon: FileOutlined,
                     icon: FileOutlined,
                     hideChildren: true
                     hideChildren: true
@@ -75,15 +73,15 @@ export const routes = [
             },
             },
             {
             {
                 path: 'config/:name',
                 path: 'config/:name',
-                name: $gettext('Edit Configuration'),
-                // component: () => import('@/views/config/ConfigEdit.vue'),
+                name: () => $gettext('Edit Configuration'),
+                component: () => import('@/views/config/ConfigEdit.vue'),
                 meta: {
                 meta: {
                     hiddenInSidebar: true
                     hiddenInSidebar: true
                 },
                 },
             },
             },
             {
             {
                 path: 'terminal',
                 path: 'terminal',
-                name: $gettext('Terminal'),
+                name: () => $gettext('Terminal'),
                 component: () => import('@/views/pty/Terminal.vue'),
                 component: () => import('@/views/pty/Terminal.vue'),
                 meta: {
                 meta: {
                     icon: CodeOutlined
                     icon: CodeOutlined
@@ -91,7 +89,7 @@ export const routes = [
             },
             },
             {
             {
                 path: 'about',
                 path: 'about',
-                name: $gettext('About'),
+                name: () => $gettext('About'),
                 component: () => import('@/views/other/About.vue'),
                 component: () => import('@/views/other/About.vue'),
                 meta: {
                 meta: {
                     icon: InfoCircleOutlined
                     icon: InfoCircleOutlined
@@ -99,27 +97,27 @@ export const routes = [
             },
             },
         ]
         ]
     },
     },
-    // {
-    //     path: '/install',
-    //     name: $gettext('Install'),
-    //     component: () => import('@/views/other/Install.vue'),
-    //     meta: {noAuth: true}
-    // },
+    {
+        path: '/install',
+        name: () => $gettext('Install'),
+        // component: () => import('@/views/other/Install.vue'),
+        meta: {noAuth: true}
+    },
     {
     {
         path: '/login',
         path: '/login',
-        name: $gettext('Login'),
+        name: () => $gettext('Login'),
         component: () => import('@/views/other/Login.vue'),
         component: () => import('@/views/other/Login.vue'),
         meta: {noAuth: true}
         meta: {noAuth: true}
     },
     },
     {
     {
         path: '/404',
         path: '/404',
-        name: $gettext('404 Not Found'),
+        name: () => $gettext('404 Not Found'),
         component: () => import('@/views/other/Error.vue'),
         component: () => import('@/views/other/Error.vue'),
         meta: {noAuth: true, status_code: 404, error: 'Not Found'}
         meta: {noAuth: true, status_code: 404, error: 'Not Found'}
     },
     },
     {
     {
         path: '/*',
         path: '/*',
-        name: $gettext('Not Found'),
+        name: () => $gettext('Not Found'),
         redirect: '/404',
         redirect: '/404',
         meta: {noAuth: true}
         meta: {noAuth: true}
     }
     }
@@ -132,7 +130,9 @@ const router = createRouter({
 })
 })
 
 
 router.beforeEach((to, from, next) => {
 router.beforeEach((to, from, next) => {
-    document.title = to.name as string + ' | Nginx UI'
+
+    // @ts-ignore
+    document.title = to.name() + ' | Nginx UI'
 
 
     if (import.meta.env.MODE === 'production') {
     if (import.meta.env.MODE === 'production') {
         // axios.get('/version.json?' + Date.now()).then(r => {
         // axios.get('/version.json?' + Date.now()).then(r => {

+ 1 - 0
frontend-next/src/style.less

@@ -1,2 +1,3 @@
 @import "ant-design-vue/dist/antd.variable";
 @import "ant-design-vue/dist/antd.variable";
+
 @border-radius-base: 4px;
 @border-radius-base: 4px;

+ 27 - 36
frontend-next/src/views/config/Config.vue

@@ -1,3 +1,30 @@
+<script setup lang="ts">
+import StdTable from '@/components/StdDataDisplay/StdTable.vue'
+import gettext from '@/gettext'
+import config from '@/api/config'
+import {datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+
+const {$gettext} = gettext
+
+const api = config
+
+const columns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'modify',
+    customRender: datetime,
+    datetime: true,
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action'
+}]
+</script>
 <template>
 <template>
     <a-card :title="$gettext('Configurations')">
     <a-card :title="$gettext('Configurations')">
         <std-table
         <std-table
@@ -5,7 +32,6 @@
             :columns="columns"
             :columns="columns"
             :deletable="false"
             :deletable="false"
             :disable_search="true"
             :disable_search="true"
-            data_key="configs"
             row-key="name"
             row-key="name"
             @clickEdit="r => {
             @clickEdit="r => {
                 $router.push({
                 $router.push({
@@ -16,41 +42,6 @@
     </a-card>
     </a-card>
 </template>
 </template>
 
 
-<script>
-import StdTable from '@/components/StdDataDisplay/StdTable'
-import $gettext from '@/lib/translate/gettext'
-
-const columns = [{
-    title: $gettext('Name'),
-    dataIndex: 'name',
-    scopedSlots: {customRender: 'name'},
-    sorter: true,
-    pithy: true
-}, {
-    title: $gettext('Updated at'),
-    dataIndex: 'modify',
-    datetime: true,
-    scopedSlots: {customRender: 'modify'},
-    sorter: true,
-    pithy: true
-}, {
-    title: $gettext('Action'),
-    dataIndex: 'action',
-    scopedSlots: {customRender: 'action'}
-}]
-
-export default {
-    name: 'Config',
-    components: {StdTable},
-    data() {
-        return {
-            api: this.$api.config,
-            columns
-        }
-    }
-}
-</script>
-
 <style scoped>
 <style scoped>
 
 
 </style>
 </style>

+ 43 - 55
frontend-next/src/views/config/ConfigEdit.vue

@@ -1,6 +1,48 @@
+<script setup lang="ts">
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import gettext from '@/gettext'
+import {useRoute} from 'vue-router'
+import {ref} from 'vue'
+import config from '@/api/config'
+import {message} from 'ant-design-vue'
+import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+
+const {$gettext, interpolate} = gettext
+const route = useRoute()
+
+const name = ref(route.params.name)
+
+const configText = ref('')
+
+function init() {
+    if (name.value) {
+        config.get(name.value).then(r => {
+            configText.value = r.config
+        }).catch(r => {
+            message.error(r.message ?? $gettext('Server error'))
+        })
+    } else {
+        configText.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 ?? ''}))
+    })
+}
+
+</script>
+
+
 <template>
 <template>
     <a-card :title="$gettext('Edit Configuration')">
     <a-card :title="$gettext('Edit Configuration')">
-        <vue-itextarea v-model="configText"/>
+        <code-editor v-model:content="configText"/>
         <footer-tool-bar>
         <footer-tool-bar>
             <a-space>
             <a-space>
                 <a-button @click="$router.go(-1)">
                 <a-button @click="$router.go(-1)">
@@ -14,60 +56,6 @@
     </a-card>
     </a-card>
 </template>
 </template>
 
 
-<script>
-import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
-import VueItextarea from '@/components/VueItextarea/VueItextarea'
-import $gettext, {$interpolate} from '@/lib/translate/gettext'
-
-export default {
-    name: 'DomainEdit',
-    components: {FooterToolBar, VueItextarea},
-    data() {
-        return {
-            name: this.$route.params.name,
-            configText: ''
-        }
-    },
-    watch: {
-        '$route'() {
-            this.init()
-        },
-        config: {
-            handler() {
-                this.unparse()
-            },
-            deep: true
-        }
-    },
-    created() {
-        this.init()
-    },
-    methods: {
-        init() {
-            if (this.name) {
-                this.$api.config.get(this.name).then(r => {
-                    this.configText = r.config
-                }).catch(r => {
-                    console.log(r)
-                    this.$message.error($gettext('Server error'))
-                })
-            } else {
-                this.configText = ''
-            }
-        },
-        save() {
-            this.$api.config.save(this.name ? this.name : this.config.name, {content: this.configText}).then(r => {
-                this.configText = r.config
-                this.$message.success($gettext('Saved successfully'))
-            }).catch(r => {
-                console.log(r)
-                this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}))
-            })
-        }
-    }
-}
-</script>
-
 <style lang="less" scoped>
 <style lang="less" scoped>
 .ant-card {
 .ant-card {
     margin: 10px;
     margin: 10px;

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

@@ -3,10 +3,10 @@ import AreaChart from '@/components/Chart/AreaChart.vue'
 
 
 import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
 import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
 import {useGettext} from 'vue3-gettext'
 import {useGettext} from 'vue3-gettext'
-import {onMounted, onUnmounted, reactive, ref} from "vue"
-import analytic from "@/api/analytic"
-import ws from "@/lib/websocket"
-import {bytesToSize} from "@/lib/helper"
+import {onMounted, onUnmounted, reactive, ref} from 'vue'
+import analytic from '@/api/analytic'
+import ws from '@/lib/websocket'
+import {bytesToSize} from '@/lib/helper'
 
 
 const {$gettext} = useGettext()
 const {$gettext} = useGettext()
 
 

+ 117 - 131
frontend-next/src/views/domain/DomainEdit.vue

@@ -1,8 +1,116 @@
+<script setup lang="ts">
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+
+// import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
+import {useGettext} from 'vue3-gettext'
+import {reactive, ref} from 'vue'
+import {useRoute} from 'vue-router'
+import domain from '@/api/domain'
+import ngx from '@/api/ngx'
+import {message} from 'ant-design-vue'
+
+
+const {$gettext, interpolate} = useGettext()
+
+const route = useRoute()
+
+const name = ref(route.params.name.toString())
+const update = ref(0)
+const ngx_config = reactive({
+    filename: '',
+    upstreams: [],
+    servers: []
+})
+
+const auto_cert = ref(false)
+const enabled = ref(false)
+const configText = ref('')
+const ok = ref(false)
+const advance_mode = ref(false)
+const saving = ref(false)
+
+init()
+
+function init() {
+    if (name.value) {
+        domain.get(name.value).then((r: any) => {
+            configText.value = r.config
+            enabled.value = r.enabled
+            auto_cert.value = r.auto_cert
+            Object.assign(ngx_config, r.tokenized)
+        }).catch(r => {
+            message.error(r.message ?? $gettext('Server error'))
+        })
+    }
+}
+
+function on_mode_change(advance_mode: boolean) {
+    if (advance_mode) {
+        build_config()
+    } else {
+        return ngx.tokenize_config(configText.value).then((r: any) => {
+            Object.assign(ngx_config, r.tokenized)
+        }).catch((e: any) => {
+            message.error(e?.message ?? $gettext('Server error'))
+        })
+    }
+}
+
+function build_config() {
+    return ngx.build_config(ngx_config).then((r: any) => {
+        configText.value = r.content
+    }).catch((e: any) => {
+        message.error(e?.message ?? $gettext('Server error'))
+    })
+}
+
+const save = async () => {
+    saving.value = true
+
+    if (!advance_mode.value) {
+        await build_config()
+    }
+
+    domain.save(name.value, {content: configText.value}).then(r => {
+        configText.value = r.config
+        enabled.value = r.enabled
+        Object.assign(ngx_config, r.tokenized)
+        message.success($gettext('Saved successfully'))
+
+        // TODO this.$refs.ngx_config.update_cert_info()
+
+    }).catch((e: any) => {
+        message.error(e?.message ?? $gettext('Server error'))
+    }).finally(() => {
+        saving.value = false
+    })
+
+}
+
+function enable() {
+    domain.enable(name.value).then(() => {
+        message.success($gettext('Enabled successfully'))
+        enabled.value = true
+    }).catch(r => {
+        message.error(interpolate($gettext('Failed to enable %{msg}'), {msg: r.message ?? ''}), 10)
+    })
+}
+
+function disable() {
+    domain.disable(name.value).then(() => {
+        message.success($gettext('Disabled successfully'))
+        enabled.value = false
+    }).catch(r => {
+        message.error(interpolate($gettext('Failed to disable %{msg}'), {msg: r.message ?? ''}))
+    })
+}
+</script>
 <template>
 <template>
     <div>
     <div>
         <a-card :bordered="false">
         <a-card :bordered="false">
             <template v-slot:title>
             <template v-slot:title>
-                <span style="margin-right: 10px">{{ $gettextInterpolate($gettext('Edit %{n}'), {n: name}) }}</span>
+                <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), {n: name}) }}</span>
                 <a-tag color="blue" v-if="enabled">
                 <a-tag color="blue" v-if="enabled">
                     {{ $gettext('Enabled') }}
                     {{ $gettext('Enabled') }}
                 </a-tag>
                 </a-tag>
@@ -11,7 +119,7 @@
                 </a-tag>
                 </a-tag>
             </template>
             </template>
             <template v-slot:extra>
             <template v-slot:extra>
-                <a-switch size="small" v-model="advance_mode" @change="on_mode_change"/>
+                <a-switch size="small" v-model:checked="advance_mode" @change="on_mode_change"/>
                 <template v-if="advance_mode">
                 <template v-if="advance_mode">
                     {{ $gettext('Advance Mode') }}
                     {{ $gettext('Advance Mode') }}
                 </template>
                 </template>
@@ -22,7 +130,7 @@
 
 
             <transition name="slide-fade">
             <transition name="slide-fade">
                 <div v-if="advance_mode" key="advance">
                 <div v-if="advance_mode" key="advance">
-                    <vue-itextarea v-model="configText"/>
+                    <code-editor v-model:content="configText"/>
                 </div>
                 </div>
 
 
                 <div class="domain-edit-container" key="basic" v-else>
                 <div class="domain-edit-container" key="basic" v-else>
@@ -30,12 +138,12 @@
                         <a-switch v-model="enabled" @change="checked=>{checked?enable():disable()}"/>
                         <a-switch v-model="enabled" @change="checked=>{checked?enable():disable()}"/>
                     </a-form-item>
                     </a-form-item>
 
 
-                    <ngx-config-editor
-                        ref="ngx_config"
-                        :ngx_config="ngx_config"
-                        v-model="auto_cert"
-                        :enabled="enabled"
-                    />
+                    <!--                    <ngx-config-editor-->
+                    <!--                        ref="ngx_config"-->
+                    <!--                        :ngx_config="ngx_config"-->
+                    <!--                        v-model="auto_cert"-->
+                    <!--                        :enabled="enabled"-->
+                    <!--                    />-->
                 </div>
                 </div>
             </transition>
             </transition>
 
 
@@ -54,128 +162,6 @@
     </div>
     </div>
 </template>
 </template>
 
 
-
-<script>
-import FooterToolBar from '@/components/FooterToolbar/FooterToolBar'
-import VueItextarea from '@/components/VueItextarea/VueItextarea'
-import {$gettext, $interpolate} from '@/lib/translate/gettext'
-import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
-
-
-export default {
-    name: 'DomainEdit',
-    components: {NgxConfigEditor, FooterToolBar, VueItextarea},
-    data() {
-        return {
-            name: this.$route.params.name.toString(),
-            update: 0,
-            ngx_config: {
-                filename: '',
-                upstreams: [],
-                servers: []
-            },
-            auto_cert: false,
-            current_server_index: 0,
-            enabled: false,
-            configText: '',
-            ws: null,
-            ok: false,
-            issuing_cert: false,
-            advance_mode: false,
-            saving: false
-        }
-    },
-    watch: {
-        '$route'() {
-            this.init()
-        },
-    },
-    created() {
-        this.init()
-    },
-    destroyed() {
-        if (this.ws !== null) {
-            this.ws.close()
-        }
-    },
-    methods: {
-        init() {
-            if (this.name) {
-                this.$api.domain.get(this.name).then(r => {
-                    this.configText = r.config
-                    this.enabled = r.enabled
-                    this.ngx_config = r.tokenized
-                    this.auto_cert = r.auto_cert
-                }).catch(r => {
-                    this.$message.error(r.message ?? $gettext('Server error'))
-                })
-            }
-        },
-        on_mode_change(advance_mode) {
-            if (advance_mode) {
-                this.build_config()
-            } else {
-                return this.$api.ngx.tokenize_config(this.configText).then(r => {
-                    this.ngx_config = r
-                }).catch(r => {
-                    this.$message.error(r.message ?? $gettext('Server error'))
-                })
-            }
-        },
-        build_config() {
-            return this.$api.ngx.build_config(this.ngx_config).then(r => {
-                this.configText = r.content
-            }).catch(r => {
-                this.$message.error(r.message ?? $gettext('Server error'))
-            })
-        },
-        async save() {
-            this.saving = true
-
-            if (!this.advance_mode) {
-                await this.build_config()
-            }
-
-            this.$api.domain.save(this.name, {content: this.configText}).then(r => {
-                this.configText = r.config
-                this.enabled = r.enabled
-                this.ngx_config = r.tokenized
-                this.$message.success($gettext('Saved successfully'))
-
-                this.$refs.ngx_config.update_cert_info()
-
-            }).catch(r => {
-                this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}), 10)
-            }).finally(() => {
-                this.saving = false
-            })
-
-        },
-        enable() {
-            this.$api.domain.enable(this.name).then(() => {
-                this.$message.success($gettext('Enabled successfully'))
-                this.enabled = true
-            }).catch(r => {
-                this.$message.error($interpolate($gettext('Failed to enable %{msg}'), {msg: r.message ?? ''}), 10)
-            })
-        },
-        disable() {
-            this.$api.domain.disable(this.name).then(() => {
-                this.$message.success($gettext('Disabled successfully'))
-                this.enabled = false
-            }).catch(r => {
-                this.$message.error($interpolate($gettext('Failed to disable %{msg}'), {msg: r.message ?? ''}))
-            })
-        }
-    },
-    computed: {
-        is_demo() {
-            return this.$store.getters.env.demo === true
-        }
-    }
-}
-</script>
-
 <style lang="less">
 <style lang="less">
 
 
 </style>
 </style>

+ 73 - 68
frontend-next/src/views/domain/DomainList.vue

@@ -1,9 +1,80 @@
+<script setup lang="tsx">
+import StdTable from '@/components/StdDataDisplay/StdTable.vue'
+
+import {badge, customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import {useGettext} from 'vue3-gettext'
+
+const {$gettext, interpolate} = useGettext()
+
+import domain from '@/api/domain'
+import {Badge, message} from 'ant-design-vue'
+import {h, ref} from 'vue'
+
+const columns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Status'),
+    dataIndex: 'enabled',
+    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="error"/>)
+            template.push($gettext('Disabled'))
+        }
+        return h('div', template)
+    },
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'modify',
+    customRender: datetime,
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action',
+}]
+
+const table = ref(null)
+
+interface Table {
+    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)
+    })
+}
+
+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 ?? ''}))
+    })
+}
+</script>
+
 <template>
 <template>
     <a-card :title="$gettext('Manage Sites')">
     <a-card :title="$gettext('Manage Sites')">
         <std-table
         <std-table
-            :api="api"
+            :api="domain"
             :columns="columns"
             :columns="columns"
-            data_key="configs"
             :disable_search="true"
             :disable_search="true"
             row-key="name"
             row-key="name"
             ref="table"
             ref="table"
@@ -24,72 +95,6 @@
     </a-card>
     </a-card>
 </template>
 </template>
 
 
-<script>
-import StdTable from '@/components/StdDataDisplay/StdTable'
-import $gettext, {$interpolate} from '@/lib/translate/gettext'
-
-const columns = [{
-    title: $gettext('Name'),
-    dataIndex: 'name',
-    scopedSlots: {customRender: 'name'},
-    sorter: true,
-    pithy: true
-}, {
-    title: $gettext('Status'),
-    dataIndex: 'enabled',
-    badge: true,
-    scopedSlots: {customRender: 'enabled'},
-    mask: {
-        true: $gettext('Enabled'),
-        false: $gettext('Disabled')
-    },
-    sorter: true,
-    pithy: true
-}, {
-    title: $gettext('Updated at'),
-    dataIndex: 'modify',
-    datetime: true,
-    scopedSlots: {customRender: 'modify'},
-    sorter: true,
-    pithy: true
-}, {
-    title: $gettext('Action'),
-    dataIndex: 'action',
-    scopedSlots: {customRender: 'action'}
-}]
-
-export default {
-    name: 'Domain',
-    components: {StdTable},
-    data() {
-        return {
-            api: this.$api.domain,
-            columns
-        }
-    },
-    methods: {
-        enable(name) {
-            this.$api.domain.enable(name).then(() => {
-                this.$message.success($gettext('Enabled successfully'))
-                this.$refs.table.get_list()
-            }).catch(r => {
-                console.log(r)
-                this.$message.error($interpolate($gettext('Failed to enable %{msg}'), {msg: r.message ?? ''}), 10)
-            })
-        },
-        disable(name) {
-            this.$api.domain.disable(name).then(() => {
-                this.$message.success($gettext('Disabled successfully'))
-                this.$refs.table.get_list()
-            }).catch(r => {
-                console.log(r)
-                this.$message.error($interpolate($gettext('Failed to disable %{msg}'), {msg: r.message ?? ''}))
-            })
-        }
-    }
-}
-</script>
-
 <style scoped>
 <style scoped>
 
 
 </style>
 </style>

+ 1 - 1
frontend-next/src/views/domain/ngx_conf/ngx_constant.js

@@ -1 +1 @@
-export const If = "if"
+export const If = 'if'

+ 3 - 2
frontend-next/src/views/other/About.vue

@@ -1,8 +1,9 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import gettext from '@/gettext'
 import gettext from '@/gettext'
-const {$gettext} = gettext
 import logo from '@/assets/img/logo.png'
 import logo from '@/assets/img/logo.png'
 
 
+const {$gettext} = gettext
+
 const this_year = new Date().getFullYear()
 const this_year = new Date().getFullYear()
 const version = import.meta.env.VITE_APP_VERSION
 const version = import.meta.env.VITE_APP_VERSION
 const build_id = import.meta.env.VITE_APP_TOTAL_BUILD ?? $gettext('Development Mode')
 const build_id = import.meta.env.VITE_APP_TOTAL_BUILD ?? $gettext('Development Mode')
@@ -18,7 +19,7 @@ const api_root = import.meta.env.VITE_API_ROOT
         <p>Yet another WebUI for Nginx</p>
         <p>Yet another WebUI for Nginx</p>
         <p>Version: {{ version }} ({{ build_id }})</p>
         <p>Version: {{ version }} ({{ build_id }})</p>
         <h3 v-translate>Project Team</h3>
         <h3 v-translate>Project Team</h3>
-        <p><a href="https://jackyu.cn/">@0xJacky</a>  <a href="https://blog.kugeek.com/">@Hintay</a></p>
+        <p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a></p>
         <h3 v-translate>Build with</h3>
         <h3 v-translate>Build with</h3>
         <p>❤️</p>
         <p>❤️</p>
         <p>Go</p>
         <p>Go</p>

+ 3 - 2
frontend-next/src/views/other/Install.vue

@@ -72,14 +72,15 @@
             </a-form-item>
             </a-form-item>
         </a-form>
         </a-form>
         <footer>
         <footer>
-            Copyright © 2020 - {{ thisYear }} Nginx UI | Language <set-language class="set_lang" style="display: inline"/>
+            Copyright © 2020 - {{ thisYear }} Nginx UI | Language
+            <set-language class="set_lang" style="display: inline"/>
         </footer>
         </footer>
     </div>
     </div>
 
 
 </template>
 </template>
 
 
 <script>
 <script>
-import SetLanguage from "@/components/SetLanguage/SetLanguage";
+import SetLanguage from '@/components/SetLanguage/SetLanguage'
 
 
 export default {
 export default {
     name: 'Login',
     name: 'Login',

+ 9 - 10
frontend-next/src/views/other/Login.vue

@@ -1,12 +1,11 @@
 <script setup lang="ts">
 <script setup lang="ts">
 const thisYear = new Date().getFullYear()
 const thisYear = new Date().getFullYear()
 
 
-import app from '@/main'
-import {UserOutlined, LockOutlined} from '@ant-design/icons-vue'
+import {LockOutlined, UserOutlined} from '@ant-design/icons-vue'
 import {reactive, ref} from 'vue'
 import {reactive, ref} from 'vue'
-import {useRouter, useRoute} from "vue-router"
-import gettext from "@/gettext"
-import {Form, message} from "ant-design-vue"
+import {useRoute, useRouter} from 'vue-router'
+import gettext from '@/gettext'
+import {Form, message} from 'ant-design-vue'
 import auth from '@/api/auth'
 import auth from '@/api/auth'
 
 
 const route = useRoute()
 const route = useRoute()
@@ -40,12 +39,11 @@ const {validate, validateInfos} = Form.useForm(modelRef, rulesRef)
 const onSubmit = () => {
 const onSubmit = () => {
     validate().then(() => {
     validate().then(() => {
         // modelRef
         // modelRef
-        auth.login(modelRef.username, modelRef.password).then(async ()=>{
+        auth.login(modelRef.username, modelRef.password).then(async () => {
             message.success($gettext('Login successful'), 1)
             message.success($gettext('Login successful'), 1)
-            const next = (route.query?.next||'').toString() || '/'
+            const next = (route.query?.next || '').toString() || '/'
             await router.push(next)
             await router.push(next)
-        }).
-        catch(e=>{
+        }).catch(e => {
             message.error(e.message)
             message.error(e.message)
         })
         })
     })
     })
@@ -88,7 +86,8 @@ const onSubmit = () => {
             </a-form>
             </a-form>
             <div class="footer">
             <div class="footer">
                 <p>Copyright © 2020 - {{ thisYear }} Nginx UI</p>
                 <p>Copyright © 2020 - {{ thisYear }} Nginx UI</p>
-                Language <set-language class="set_lang" style="display: inline"/>
+                Language
+                <set-language class="set_lang" style="display: inline"/>
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>

+ 2 - 2
frontend-next/src/views/pty/Terminal.vue

@@ -2,9 +2,9 @@
 import 'xterm/css/xterm.css'
 import 'xterm/css/xterm.css'
 import {Terminal} from 'xterm'
 import {Terminal} from 'xterm'
 import {FitAddon} from 'xterm-addon-fit'
 import {FitAddon} from 'xterm-addon-fit'
-import {onMounted, onUnmounted} from "vue"
+import {onMounted, onUnmounted} from 'vue'
 import _ from 'lodash'
 import _ from 'lodash'
-import ws from "@/lib/websocket"
+import ws from '@/lib/websocket'
 
 
 let term: Terminal | null
 let term: Terminal | null
 let ping: null | NodeJS.Timer
 let ping: null | NodeJS.Timer

+ 11 - 19
frontend-next/src/views/user/User.vue

@@ -1,10 +1,9 @@
-<template>
-    <std-curd :columns="columns" :api="api" :disable_search="true"/>
-</template>
-
-<script lang="ts">
+<script setup lang="ts">
 import StdCurd from '@/components/StdDataDisplay/StdCurd.vue'
 import StdCurd from '@/components/StdDataDisplay/StdCurd.vue'
 import gettext from '@/gettext'
 import gettext from '@/gettext'
+import user from '@/api/user'
+import {datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+
 const {$gettext} = gettext
 const {$gettext} = gettext
 
 
 const columns = [{
 const columns = [{
@@ -28,33 +27,26 @@ const columns = [{
 }, {
 }, {
     title: $gettext('Created at'),
     title: $gettext('Created at'),
     dataIndex: 'created_at',
     dataIndex: 'created_at',
-    datetime: true,
+    customRender: datetime,
     sorter: true,
     sorter: true,
     pithy: true
     pithy: true
 }, {
 }, {
     title: $gettext('Updated at'),
     title: $gettext('Updated at'),
     dataIndex: 'updated_at',
     dataIndex: 'updated_at',
-    datetime: true,
+    customRender: datetime,
     sorter: true,
     sorter: true,
     pithy: true
     pithy: true
 }, {
 }, {
     title: $gettext('Action'),
     title: $gettext('Action'),
     dataIndex: 'action'
     dataIndex: 'action'
 }]
 }]
-
-export default {
-    name: 'User',
-    components: {StdCurd},
-    data() {
-        return {
-            api: this.$api.user,
-            columns
-        }
-    },
-    methods: {}
-}
 </script>
 </script>
 
 
+<template>
+    <std-curd :columns="columns" :api="user" :disable_search="true"/>
+</template>
+
+
 <style scoped>
 <style scoped>
 
 
 </style>
 </style>

+ 3 - 3
frontend-next/src/vite-env.d.ts

@@ -1,7 +1,7 @@
 /// <reference types="vite/client" />
 /// <reference types="vite/client" />
 
 
 declare module '*.vue' {
 declare module '*.vue' {
-  import type { DefineComponent } from 'vue'
-  const component: DefineComponent<{}, {}, any>
-  export default component
+    import type {DefineComponent} from 'vue'
+    const component: DefineComponent<{}, {}, any>
+    export default component
 }
 }

+ 3 - 1
frontend-next/tsconfig.json

@@ -17,7 +17,9 @@
         "skipLibCheck": true,
         "skipLibCheck": true,
         "baseUrl": ".",
         "baseUrl": ".",
         "paths": {
         "paths": {
-            "@/*": ["./src/*"]
+            "@/*": [
+                "./src/*"
+            ]
         }
         }
     },
     },
     "include": [
     "include": [

+ 9 - 7
frontend-next/tsconfig.node.json

@@ -1,9 +1,11 @@
 {
 {
-  "compilerOptions": {
-    "composite": true,
-    "module": "ESNext",
-    "moduleResolution": "Node",
-    "allowSyntheticDefaultImports": true
-  },
-  "include": ["vite.config.ts"]
+    "compilerOptions": {
+        "composite": true,
+        "module": "ESNext",
+        "moduleResolution": "Node",
+        "allowSyntheticDefaultImports": true
+    },
+    "include": [
+        "vite.config.ts"
+    ]
 }
 }

+ 5 - 3
frontend-next/vite.config.ts

@@ -3,13 +3,15 @@ import vue from '@vitejs/plugin-vue'
 import {createHtmlPlugin} from 'vite-plugin-html'
 import {createHtmlPlugin} from 'vite-plugin-html'
 import Components from 'unplugin-vue-components/vite'
 import Components from 'unplugin-vue-components/vite'
 import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'
 import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'
-import {fileURLToPath, URL} from "url"
+import {fileURLToPath, URL} from 'url'
+import vueJsx from '@vitejs/plugin-vue-jsx'
+
 
 
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
     resolve: {
     resolve: {
         alias: {
         alias: {
-            "@": fileURLToPath(new URL("./src", import.meta.url)),
+            '@': fileURLToPath(new URL('./src', import.meta.url)),
         },
         },
         extensions: [
         extensions: [
             '.mjs',
             '.mjs',
@@ -22,7 +24,7 @@ export default defineConfig({
             '.less'
             '.less'
         ]
         ]
     },
     },
-    plugins: [vue(),
+    plugins: [vue(), vueJsx(),
         Components({
         Components({
             resolvers: [AntDesignVueResolver({importStyle: false})]
             resolvers: [AntDesignVueResolver({importStyle: false})]
         }),
         }),

+ 352 - 13
frontend-next/yarn.lock

@@ -2,6 +2,14 @@
 # yarn lockfile v1
 # yarn lockfile v1
 
 
 
 
+"@ampproject/remapping@^2.1.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
+  integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.1.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
 "@ant-design/colors@^6.0.0":
 "@ant-design/colors@^6.0.0":
   version "6.0.0"
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
   resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
@@ -27,18 +35,182 @@
   resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.5.2.tgz#8c2d931ff927be0ebe740169874a3d4004ab414b"
   resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.5.2.tgz#8c2d931ff927be0ebe740169874a3d4004ab414b"
   integrity sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==
   integrity sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==
 
 
-"@babel/code-frame@^7.0.0":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6":
   version "7.18.6"
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
   integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
   integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
   dependencies:
   dependencies:
     "@babel/highlight" "^7.18.6"
     "@babel/highlight" "^7.18.6"
 
 
+"@babel/compat-data@^7.18.8":
+  version "7.18.8"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
+  integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==
+
+"@babel/core@^7.18.6":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.9.tgz#805461f967c77ff46c74ca0460ccf4fe933ddd59"
+  integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==
+  dependencies:
+    "@ampproject/remapping" "^2.1.0"
+    "@babel/code-frame" "^7.18.6"
+    "@babel/generator" "^7.18.9"
+    "@babel/helper-compilation-targets" "^7.18.9"
+    "@babel/helper-module-transforms" "^7.18.9"
+    "@babel/helpers" "^7.18.9"
+    "@babel/parser" "^7.18.9"
+    "@babel/template" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.2"
+    json5 "^2.2.1"
+    semver "^6.3.0"
+
+"@babel/generator@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.9.tgz#68337e9ea8044d6ddc690fb29acae39359cca0a5"
+  integrity sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==
+  dependencies:
+    "@babel/types" "^7.18.9"
+    "@jridgewell/gen-mapping" "^0.3.2"
+    jsesc "^2.5.1"
+
+"@babel/helper-annotate-as-pure@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
+  integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
+"@babel/helper-compilation-targets@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf"
+  integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==
+  dependencies:
+    "@babel/compat-data" "^7.18.8"
+    "@babel/helper-validator-option" "^7.18.6"
+    browserslist "^4.20.2"
+    semver "^6.3.0"
+
+"@babel/helper-create-class-features-plugin@^7.18.6":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz#d802ee16a64a9e824fcbf0a2ffc92f19d58550ce"
+  integrity sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.18.6"
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-function-name" "^7.18.9"
+    "@babel/helper-member-expression-to-functions" "^7.18.9"
+    "@babel/helper-optimise-call-expression" "^7.18.6"
+    "@babel/helper-replace-supers" "^7.18.9"
+    "@babel/helper-split-export-declaration" "^7.18.6"
+
+"@babel/helper-environment-visitor@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
+  integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
+
+"@babel/helper-function-name@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0"
+  integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==
+  dependencies:
+    "@babel/template" "^7.18.6"
+    "@babel/types" "^7.18.9"
+
+"@babel/helper-hoist-variables@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
+  integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
+"@babel/helper-member-expression-to-functions@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
+  integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==
+  dependencies:
+    "@babel/types" "^7.18.9"
+
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
+  integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
+"@babel/helper-module-transforms@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712"
+  integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==
+  dependencies:
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-module-imports" "^7.18.6"
+    "@babel/helper-simple-access" "^7.18.6"
+    "@babel/helper-split-export-declaration" "^7.18.6"
+    "@babel/helper-validator-identifier" "^7.18.6"
+    "@babel/template" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+
+"@babel/helper-optimise-call-expression@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe"
+  integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
+"@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.18.6":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f"
+  integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==
+
+"@babel/helper-replace-supers@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz#1092e002feca980fbbb0bd4d51b74a65c6a500e6"
+  integrity sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ==
+  dependencies:
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-member-expression-to-functions" "^7.18.9"
+    "@babel/helper-optimise-call-expression" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+
+"@babel/helper-simple-access@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea"
+  integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
+"@babel/helper-split-export-declaration@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
+  integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
 "@babel/helper-validator-identifier@^7.18.6":
 "@babel/helper-validator-identifier@^7.18.6":
   version "7.18.6"
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
   integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
   integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
 
 
+"@babel/helper-validator-option@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
+  integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
+
+"@babel/helpers@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9"
+  integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==
+  dependencies:
+    "@babel/template" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+
 "@babel/highlight@^7.18.6":
 "@babel/highlight@^7.18.6":
   version "7.18.6"
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
@@ -48,11 +220,41 @@
     chalk "^2.0.0"
     chalk "^2.0.0"
     js-tokens "^4.0.0"
     js-tokens "^4.0.0"
 
 
-"@babel/parser@^7.16.4":
+"@babel/parser@^7.16.4", "@babel/parser@^7.18.6", "@babel/parser@^7.18.9":
   version "7.18.9"
   version "7.18.9"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
   integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
   integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
 
 
+"@babel/plugin-syntax-import-meta@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
+  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-jsx@^7.0.0":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
+  integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-syntax-typescript@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285"
+  integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.18.6"
+
+"@babel/plugin-transform-typescript@^7.18.8":
+  version "7.18.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.8.tgz#303feb7a920e650f2213ef37b36bbf327e6fa5a0"
+  integrity sha512-p2xM8HI83UObjsZGofMV/EdYjamsDm6MoN3hXPYIT0+gxIoopE+B7rPYKAxfrz9K9PK7JafTTjqYC6qipLExYA==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.18.6"
+    "@babel/helper-plugin-utils" "^7.18.6"
+    "@babel/plugin-syntax-typescript" "^7.18.6"
+
 "@babel/runtime@^7.10.5":
 "@babel/runtime@^7.10.5":
   version "7.18.9"
   version "7.18.9"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
@@ -60,12 +262,53 @@
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.4"
     regenerator-runtime "^0.13.4"
 
 
+"@babel/template@^7.0.0", "@babel/template@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
+  integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==
+  dependencies:
+    "@babel/code-frame" "^7.18.6"
+    "@babel/parser" "^7.18.6"
+    "@babel/types" "^7.18.6"
+
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
+  integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
+  dependencies:
+    "@babel/code-frame" "^7.18.6"
+    "@babel/generator" "^7.18.9"
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-function-name" "^7.18.9"
+    "@babel/helper-hoist-variables" "^7.18.6"
+    "@babel/helper-split-export-declaration" "^7.18.6"
+    "@babel/parser" "^7.18.9"
+    "@babel/types" "^7.18.9"
+    debug "^4.1.0"
+    globals "^11.1.0"
+
+"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f"
+  integrity sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.18.6"
+    to-fast-properties "^2.0.0"
+
 "@ctrl/tinycolor@^3.4.0":
 "@ctrl/tinycolor@^3.4.0":
   version "3.4.1"
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
   resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
   integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
   integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
 
 
-"@jridgewell/gen-mapping@^0.3.0":
+"@jridgewell/gen-mapping@^0.1.0":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
+  integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.0"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
   version "0.3.2"
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
   integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
   integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
@@ -79,7 +322,7 @@
   resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
   resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
   integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
   integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
 
 
-"@jridgewell/set-array@^1.0.1":
+"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
   version "1.1.2"
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
   resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
   integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
   integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
@@ -180,6 +423,16 @@
   resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
   resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
   integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
   integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
 
 
+"@vitejs/plugin-vue-jsx@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-2.0.0.tgz#9947c72f9ead40cb7950ba5a9e9f7ac4c5b74df1"
+  integrity sha512-WF9ApZ/ivyyW3volQfu0Td0KNPhcccYEaRNzNY1NxRLVJQLSX0nFqquv3e2g7MF74p1XZK4bGtDL2y5i5O5+1A==
+  dependencies:
+    "@babel/core" "^7.18.6"
+    "@babel/plugin-syntax-import-meta" "^7.10.4"
+    "@babel/plugin-transform-typescript" "^7.18.8"
+    "@vue/babel-plugin-jsx" "^1.1.1"
+
 "@vitejs/plugin-vue@^3.0.0":
 "@vitejs/plugin-vue@^3.0.0":
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.1.tgz#b6af8f782485374bbb5fe09edf067a845bf4caae"
   resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.0.1.tgz#b6af8f782485374bbb5fe09edf067a845bf4caae"
@@ -219,6 +472,26 @@
     "@vue/compiler-sfc" "^3.2.37"
     "@vue/compiler-sfc" "^3.2.37"
     "@vue/reactivity" "^3.2.37"
     "@vue/reactivity" "^3.2.37"
 
 
+"@vue/babel-helper-vue-transform-on@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc"
+  integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==
+
+"@vue/babel-plugin-jsx@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz#0c5bac27880d23f89894cd036a37b55ef61ddfc1"
+  integrity sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@babel/plugin-syntax-jsx" "^7.0.0"
+    "@babel/template" "^7.0.0"
+    "@babel/traverse" "^7.0.0"
+    "@babel/types" "^7.0.0"
+    "@vue/babel-helper-vue-transform-on" "^1.0.2"
+    camelcase "^6.0.0"
+    html-tags "^3.1.0"
+    svg-tags "^1.0.0"
+
 "@vue/compiler-core@3.2.37", "@vue/compiler-core@^3.2.37":
 "@vue/compiler-core@3.2.37", "@vue/compiler-core@^3.2.37":
   version "3.2.37"
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
   resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
@@ -339,6 +612,11 @@
     fs-extra "^10.0.0"
     fs-extra "^10.0.0"
     string-hash "^1.1.3"
     string-hash "^1.1.3"
 
 
+ace-builds@^1.4.13:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.8.1.tgz#5d318fa13d7e6ea947f8a50e42c570c573b29529"
+  integrity sha512-wjEQ4khMQYg9FfdEDoOtqdoHwcwFL48H0VB3te5b5A3eqHwxsTw8IX6+xzfisgborIb8dYU+1y9tcmtGFrCPIg==
+
 acorn@^8.5.0, acorn@^8.7.1:
 acorn@^8.5.0, acorn@^8.7.1:
   version "8.8.0"
   version "8.8.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
@@ -471,7 +749,7 @@ braces@^3.0.2, braces@~3.0.2:
   dependencies:
   dependencies:
     fill-range "^7.0.1"
     fill-range "^7.0.1"
 
 
-browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.20.3:
+browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.20.2, browserslist@^4.20.3:
   version "4.21.3"
   version "4.21.3"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
   integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
   integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
@@ -504,6 +782,11 @@ camel-case@^4.1.2:
     pascal-case "^3.1.2"
     pascal-case "^3.1.2"
     tslib "^2.0.3"
     tslib "^2.0.3"
 
 
+camelcase@^6.0.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
 caniuse-api@^3.0.0:
 caniuse-api@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@@ -665,6 +948,13 @@ consola@^2.15.3:
   resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
   resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
   integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
   integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
 
 
+convert-source-map@^1.7.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
+  integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
+  dependencies:
+    safe-buffer "~5.1.1"
+
 copy-anything@^2.0.1:
 copy-anything@^2.0.1:
   version "2.0.6"
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
   resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@@ -798,7 +1088,7 @@ csstype@^2.6.8:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
 
 
-dayjs@^1.10.5:
+dayjs@^1.10.5, dayjs@^1.11.4:
   version "1.11.4"
   version "1.11.4"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e"
   integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==
   integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==
@@ -810,7 +1100,7 @@ debug@^3.2.6:
   dependencies:
   dependencies:
     ms "^2.1.1"
     ms "^2.1.1"
 
 
-debug@^4.3.4:
+debug@^4.1.0, debug@^4.3.4:
   version "4.3.4"
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
   integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
   integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -1129,6 +1419,11 @@ function-bind@^1.1.1:
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
 
+gensync@^1.0.0-beta.2:
+  version "1.0.0-beta.2"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
 gettext-extractor@^3.5.4:
 gettext-extractor@^3.5.4:
   version "3.5.4"
   version "3.5.4"
   resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.5.4.tgz#bd36c65b4d26014ffd925f9ac7b4738d6893d6b2"
   resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.5.4.tgz#bd36c65b4d26014ffd925f9ac7b4738d6893d6b2"
@@ -1161,6 +1456,11 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
     once "^1.3.0"
     once "^1.3.0"
     path-is-absolute "^1.0.0"
     path-is-absolute "^1.0.0"
 
 
+globals@^11.1.0:
+  version "11.12.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.10"
   version "4.2.10"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
@@ -1201,6 +1501,11 @@ html-minifier-terser@^6.1.0:
     relateurl "^0.2.7"
     relateurl "^0.2.7"
     terser "^5.10.0"
     terser "^5.10.0"
 
 
+html-tags@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
+  integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==
+
 iconv-lite@^0.6.3:
 iconv-lite@^0.6.3:
   version "0.6.3"
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -1305,11 +1610,21 @@ jake@^10.8.5:
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
 
+jsesc@^2.5.1:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
 json-parse-even-better-errors@^2.3.0:
 json-parse-even-better-errors@^2.3.0:
   version "2.3.1"
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
 
+json5@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
+  integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
+
 jsonfile@^6.0.1:
 jsonfile@^6.0.1:
   version "6.1.0"
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -1461,11 +1776,6 @@ minimatch@^5.0.1, minimatch@^5.1.0:
   dependencies:
   dependencies:
     brace-expansion "^2.0.1"
     brace-expansion "^2.0.1"
 
 
-moment@^2.29.4:
-  version "2.29.4"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
-  integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
-
 ms@2.1.2:
 ms@2.1.2:
   version "2.1.2"
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1955,6 +2265,11 @@ run-parallel@^1.1.9:
   dependencies:
   dependencies:
     queue-microtask "^1.2.2"
     queue-microtask "^1.2.2"
 
 
+safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
 "safer-buffer@>= 2.1.2 < 3.0.0":
 "safer-buffer@>= 2.1.2 < 3.0.0":
   version "2.1.2"
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@@ -1977,6 +2292,11 @@ semver@^5.6.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
 
+semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
 shallow-equal@^1.0.0:
 shallow-equal@^1.0.0:
   version "1.2.1"
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
   resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
@@ -2049,6 +2369,11 @@ supports-preserve-symlinks-flag@^1.0.0:
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
 
+svg-tags@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
+  integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
+
 svg.draggable.js@^2.2.2:
 svg.draggable.js@^2.2.2:
   version "2.2.2"
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
   resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
@@ -2127,6 +2452,11 @@ terser@^5.10.0:
     commander "^2.20.0"
     commander "^2.20.0"
     source-map-support "~0.5.20"
     source-map-support "~0.5.20"
 
 
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+
 to-regex-range@^5.0.1:
 to-regex-range@^5.0.1:
   version "5.0.1"
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
   resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -2266,6 +2596,15 @@ vue-types@^3.0.0:
   dependencies:
   dependencies:
     is-plain-object "3.0.1"
     is-plain-object "3.0.1"
 
 
+vue3-ace-editor@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/vue3-ace-editor/-/vue3-ace-editor-2.2.2.tgz#7fd694df2f556e8859edd2322703e039461c9ecc"
+  integrity sha512-fZ6OWosbU+odLrtrcGC/536QjCigujYJB0Hf6/tBp+ef/ohTadwQAqyBlVzOmvrmzZyubphpV9zkaZcx5Fuivw==
+  dependencies:
+    ace-builds "^1.4.13"
+    resize-observer-polyfill "^1.5.1"
+    vue "^3.2.26"
+
 vue3-apexcharts@^1.4.1:
 vue3-apexcharts@^1.4.1:
   version "1.4.1"
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
   resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
@@ -2286,7 +2625,7 @@ vue3-gettext@^2.3.0:
     pofile "^1.1.3"
     pofile "^1.1.3"
     tslib "^2.3.1"
     tslib "^2.3.1"
 
 
-vue@^3.2.37:
+vue@^3.2.26, vue@^3.2.37:
   version "3.2.37"
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
   integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==
   integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==

+ 2 - 2
server/api/analytic.go

@@ -170,7 +170,7 @@ func GetAnalyticInit(c *gin.Context) {
 		return
 		return
 	}
 	}
 
 
-	disk, err := getDiskStat()
+	diskStat, err := getDiskStat()
 
 
 	if err != nil {
 	if err != nil {
 		log.Println(err)
 		log.Println(err)
@@ -200,6 +200,6 @@ func GetAnalyticInit(c *gin.Context) {
 			"reads":  analytic.DiskReadRecord,
 			"reads":  analytic.DiskReadRecord,
 		},
 		},
 		"memory": memory,
 		"memory": memory,
-		"disk":   disk,
+		"disk":   diskStat,
 	})
 	})
 }
 }

+ 1 - 1
server/api/config.go

@@ -45,7 +45,7 @@ func GetConfigs(c *gin.Context) {
 	configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
 	configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
 
 
 	c.JSON(http.StatusOK, gin.H{
 	c.JSON(http.StatusOK, gin.H{
-		"configs": configs,
+		"data": configs,
 	})
 	})
 }
 }
 
 

+ 235 - 235
server/api/domain.go

@@ -1,270 +1,270 @@
 package api
 package api
 
 
 import (
 import (
-    "github.com/0xJacky/Nginx-UI/server/model"
-    "github.com/0xJacky/Nginx-UI/server/tool"
-    "github.com/0xJacky/Nginx-UI/server/tool/nginx"
-    "github.com/gin-gonic/gin"
-    "io/ioutil"
-    "net/http"
-    "os"
-    "path/filepath"
-    "strings"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/tool"
+	"github.com/0xJacky/Nginx-UI/server/tool/nginx"
+	"github.com/gin-gonic/gin"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
 )
 )
 
 
 func GetDomains(c *gin.Context) {
 func GetDomains(c *gin.Context) {
-    orderBy := c.Query("order_by")
-    sort := c.DefaultQuery("sort", "desc")
-
-    mySort := map[string]string{
-        "enabled": "bool",
-        "name":    "string",
-        "modify":  "time",
-    }
-
-    configFiles, err := ioutil.ReadDir(nginx.GetNginxConfPath("sites-available"))
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    enabledConfig, err := ioutil.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    enabledConfigMap := make(map[string]bool)
-    for i := range enabledConfig {
-        enabledConfigMap[enabledConfig[i].Name()] = true
-    }
-
-    var configs []gin.H
-
-    for i := range configFiles {
-        file := configFiles[i]
-        if !file.IsDir() {
-            configs = append(configs, gin.H{
-                "name":    file.Name(),
-                "size":    file.Size(),
-                "modify":  file.ModTime(),
-                "enabled": enabledConfigMap[file.Name()],
-            })
-        }
-    }
-
-    configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
-
-    c.JSON(http.StatusOK, gin.H{
-        "configs": configs,
-    })
+	orderBy := c.Query("order_by")
+	sort := c.DefaultQuery("sort", "desc")
+
+	mySort := map[string]string{
+		"enabled": "bool",
+		"name":    "string",
+		"modify":  "time",
+	}
+
+	configFiles, err := ioutil.ReadDir(nginx.GetNginxConfPath("sites-available"))
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	enabledConfig, err := ioutil.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	enabledConfigMap := make(map[string]bool)
+	for i := range enabledConfig {
+		enabledConfigMap[enabledConfig[i].Name()] = true
+	}
+
+	var configs []gin.H
+
+	for i := range configFiles {
+		file := configFiles[i]
+		if !file.IsDir() {
+			configs = append(configs, gin.H{
+				"name":    file.Name(),
+				"size":    file.Size(),
+				"modify":  file.ModTime(),
+				"enabled": enabledConfigMap[file.Name()],
+			})
+		}
+	}
+
+	configs = tool.Sort(orderBy, sort, mySort[orderBy], configs)
+
+	c.JSON(http.StatusOK, gin.H{
+		"data": configs,
+	})
 }
 }
 
 
 func GetDomain(c *gin.Context) {
 func GetDomain(c *gin.Context) {
-    name := c.Param("name")
-    path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+	name := c.Param("name")
+	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 
 
-    enabled := true
-    if _, err := os.Stat(filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) {
-        enabled = false
-    }
+	enabled := true
+	if _, err := os.Stat(filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) {
+		enabled = false
+	}
 
 
-    config, err := nginx.ParseNgxConfig(path)
+	config, err := nginx.ParseNgxConfig(path)
 
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
 
-    _, err = model.FirstCert(name)
+	_, err = model.FirstCert(name)
 
 
-    c.JSON(http.StatusOK, gin.H{
-        "enabled":   enabled,
-        "name":      name,
-        "config":    config.BuildConfig(),
-        "tokenized": config,
-        "auto_cert": err == nil,
-    })
+	c.JSON(http.StatusOK, gin.H{
+		"enabled":   enabled,
+		"name":      name,
+		"config":    config.BuildConfig(),
+		"tokenized": config,
+		"auto_cert": err == nil,
+	})
 
 
 }
 }
 
 
 func EditDomain(c *gin.Context) {
 func EditDomain(c *gin.Context) {
-    var err error
-    name := c.Param("name")
-    request := make(gin.H)
-    err = c.BindJSON(&request)
-    path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
-
-    err = ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
-    if _, err = os.Stat(enabledConfigFilePath); err == nil {
-        // Test nginx configuration
-        err = nginx.TestNginxConf()
-        if err != nil {
-            c.JSON(http.StatusInternalServerError, gin.H{
-                "message": err.Error(),
-            })
-            return
-        }
-
-        output := nginx.ReloadNginx()
-
-        if output != "" && strings.Contains(output, "error") {
-            c.JSON(http.StatusInternalServerError, gin.H{
-                "message": output,
-            })
-            return
-        }
-    }
-
-    GetDomain(c)
+	var err error
+	name := c.Param("name")
+	request := make(gin.H)
+	err = c.BindJSON(&request)
+	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+
+	err = ioutil.WriteFile(path, []byte(request["content"].(string)), 0644)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+	if _, err = os.Stat(enabledConfigFilePath); err == nil {
+		// Test nginx configuration
+		err = nginx.TestNginxConf()
+		if err != nil {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"message": err.Error(),
+			})
+			return
+		}
+
+		output := nginx.ReloadNginx()
+
+		if output != "" && strings.Contains(output, "error") {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"message": output,
+			})
+			return
+		}
+	}
+
+	GetDomain(c)
 }
 }
 
 
 func EnableDomain(c *gin.Context) {
 func EnableDomain(c *gin.Context) {
-    configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
-    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
-
-    _, err := os.Stat(configFilePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
-        err = os.Symlink(configFilePath, enabledConfigFilePath)
-
-        if err != nil {
-            ErrHandler(c, err)
-            return
-        }
-    }
-
-    // Test nginx config, if not pass then rollback.
-    err = nginx.TestNginxConf()
-    if err != nil {
-        _ = os.Remove(enabledConfigFilePath)
-        c.JSON(http.StatusInternalServerError, gin.H{
-            "message": err.Error(),
-        })
-        return
-    }
-
-    output := nginx.ReloadNginx()
-
-    if output != "" && strings.Contains(output, "error") {
-        c.JSON(http.StatusInternalServerError, gin.H{
-            "message": output,
-        })
-        return
-    }
-
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
+	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
+
+	_, err := os.Stat(configFilePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
+		err = os.Symlink(configFilePath, enabledConfigFilePath)
+
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+	}
+
+	// Test nginx config, if not pass then rollback.
+	err = nginx.TestNginxConf()
+	if err != nil {
+		_ = os.Remove(enabledConfigFilePath)
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": err.Error(),
+		})
+		return
+	}
+
+	output := nginx.ReloadNginx()
+
+	if output != "" && strings.Contains(output, "error") {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
 }
 }
 
 
 func DisableDomain(c *gin.Context) {
 func DisableDomain(c *gin.Context) {
-    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
-
-    _, err := os.Stat(enabledConfigFilePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    err = os.Remove(enabledConfigFilePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    // delete auto cert record
-    cert := model.Cert{Domain: c.Param("name")}
-    err = cert.Remove()
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    output := nginx.ReloadNginx()
-
-    if output != "" {
-        c.JSON(http.StatusInternalServerError, gin.H{
-            "message": output,
-        })
-        return
-    }
-
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), c.Param("name"))
+
+	_, err := os.Stat(enabledConfigFilePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	err = os.Remove(enabledConfigFilePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	// delete auto cert record
+	cert := model.Cert{Domain: c.Param("name")}
+	err = cert.Remove()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	output := nginx.ReloadNginx()
+
+	if output != "" {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"message": output,
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
 }
 }
 
 
 func DeleteDomain(c *gin.Context) {
 func DeleteDomain(c *gin.Context) {
-    var err error
-    name := c.Param("name")
-    availablePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
-    enabledPath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
-
-    if _, err = os.Stat(availablePath); os.IsNotExist(err) {
-        c.JSON(http.StatusNotFound, gin.H{
-            "message": "site not found",
-        })
-        return
-    }
-
-    if _, err = os.Stat(enabledPath); err == nil {
-        c.JSON(http.StatusNotAcceptable, gin.H{
-            "message": "site is enabled",
-        })
-        return
-    }
-
-    cert := model.Cert{Domain: name}
-    _ = cert.Remove()
-
-    err = os.Remove(availablePath)
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	var err error
+	name := c.Param("name")
+	availablePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+	enabledPath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+
+	if _, err = os.Stat(availablePath); os.IsNotExist(err) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "site not found",
+		})
+		return
+	}
+
+	if _, err = os.Stat(enabledPath); err == nil {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "site is enabled",
+		})
+		return
+	}
+
+	cert := model.Cert{Domain: name}
+	_ = cert.Remove()
+
+	err = os.Remove(availablePath)
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
 
 
 }
 }
 
 
 func AddDomainToAutoCert(c *gin.Context) {
 func AddDomainToAutoCert(c *gin.Context) {
-    domain := c.Param("domain")
-    cert, err := model.FirstOrCreateCert(domain)
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-    c.JSON(http.StatusOK, cert)
+	domain := c.Param("domain")
+	cert, err := model.FirstOrCreateCert(domain)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, cert)
 }
 }
 
 
 func RemoveDomainFromAutoCert(c *gin.Context) {
 func RemoveDomainFromAutoCert(c *gin.Context) {
-    cert := model.Cert{
-        Domain: c.Param("domain"),
-    }
-    err := cert.Remove()
-
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-    c.JSON(http.StatusOK, nil)
+	cert := model.Cert{
+		Domain: c.Param("domain"),
+	}
+	err := cert.Remove()
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, nil)
 }
 }

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