Переглянути джерело

[frontend-next] Refactored configs, terminal

0xJacky 2 роки тому
батько
коміт
d2fb41fbdf
57 змінених файлів з 1565 додано та 1261 видалено
  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
 
-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
 
@@ -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
 
-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.
 
 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']
     ACol: typeof import('ant-design-vue/es')['Col']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    ADivider: typeof import('ant-design-vue/es')['Divider']
     ADrawer: typeof import('ant-design-vue/es')['Drawer']
     AForm: typeof import('ant-design-vue/es')['Form']
     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']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     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']
     ARow: typeof import('ant-design-vue/es')['Row']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
+    ASpace: typeof import('ant-design-vue/es')['Space']
     AStatistic: typeof import('ant-design-vue/es')['Statistic']
     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']
+    CodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
     FooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
     Logo: typeof import('./src/components/Logo/Logo.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"/>
     <link href="/favicon.ico" rel="icon">
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <style type="text/css">
+        #app {
+            height: 100%;
+        }
+    </style>
     <title><%- title %></title>
 </head>
 <body>

+ 3 - 1
frontend-next/package.json

@@ -15,8 +15,8 @@
         "ant-design-vue": "^3.2.10",
         "apexcharts": "^3.35.4",
         "axios": "^0.27.2",
+        "dayjs": "^1.11.4",
         "lodash": "^4.17.21",
-        "moment": "^2.29.4",
         "path": "^0.12.7",
         "pinia": "^2.0.17",
         "pinia-plugin-persistedstate": "^1.6.3",
@@ -24,6 +24,7 @@
         "vue": "^3.2.37",
         "vue-chartjs": "^4.1.1",
         "vue-router": "4",
+        "vue3-ace-editor": "^2.2.2",
         "vue3-apexcharts": "^1.4.1",
         "vue3-gettext": "^2.3.0",
         "vuex": "^4.0.2",
@@ -34,6 +35,7 @@
     "devDependencies": {
         "@types/lodash": "^4.14.182",
         "@vitejs/plugin-vue": "^3.0.0",
+        "@vitejs/plugin-vue-jsx": "^2.0.0",
         "@zougt/vite-plugin-theme-preprocessor": "^1.4.5",
         "less": "^4.1.3",
         "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
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
 //@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)')
 const callback = (media: { matches: any; }) => {
@@ -30,7 +30,5 @@ if (typeof media.addEventListener === 'function') {
 </template>
 
 <style lang="less" scoped>
-#app {
-    height: 100%;
-}
+
 </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 = {
     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 {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">
-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 {
-    name: string
+    name: any
     path: string
 }
 
-const name = ref('')
+const name = ref()
 const route = useRoute()
 
 const breadList = computed(() => {
     let _breadList: bread[] = []
 
-    name.value = (route.name || '').toString()
+    name.value = route.name
 
     route.matched.forEach(item => {
         //item.name !== 'index' && this.breadList.push(item)
         _breadList.push({
-            name: (item.name || '').toString(),
+            name: item.name,
             path: item.path
         })
     })
@@ -38,9 +35,9 @@ const breadList = computed(() => {
             <router-link
                 v-if="item.name !== name && index !== 1"
                 :to="{ path: item.path === '' ? '/' : item.path }"
-            >{{ $gettext(item.name) }}
+            >{{ item.name() }}
             </router-link>
-            <span v-else>{{ $gettext(item.name) }}</span>
+            <span v-else>{{ item.name() }}</span>
         </a-breadcrumb-item>
     </a-breadcrumb>
 </template>

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

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 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'])
 
@@ -117,7 +117,7 @@ const callback = () => {
                 },
             }
         }
-    };
+    }
     instance!.updateOptions(chartOptions)
 }
 

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

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import VueApexCharts from 'vue3-apexcharts'
-import app from '@/main'
-import {reactive} from "vue";
+import {reactive} from 'vue'
 
 const {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">
-import logo from '@/assets/img/logo.png'
-</script>
+import logo from '@/assets/img/logo.png'</script>
 
 <template>
     <div class="logo">

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

@@ -1,10 +1,7 @@
 <script setup lang="ts">
 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'])
 
@@ -16,7 +13,7 @@ const display = computed(() => {
 
 const name = ref(route.name)
 watch(() => route.name, () => {
-    name.value = (route.name || '').toString()
+    name.value = route.name
 })
 
 </script>
@@ -30,26 +27,12 @@ watch(() => route.name, () => {
                     <div class="row">
                         <img v-if="logo" :src="logo" class="logo"/>
                         <h1 class="title">
-                            {{ $gettext(name.toString()) }}
+                            {{ name() }}
                         </h1>
                         <div class="action">
                             <slot name="action"></slot>
                         </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>

+ 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">
-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 route = useRoute()
 
 const current = ref(gettext.current)
 
@@ -14,9 +17,8 @@ const languageAvailable = gettext.available
 watch(current, (v) => {
     settings.set_language(v)
     gettext.current = v
-    // nextTick(() => {
-    //     location.reload()
-    // })
+    // @ts-ignored
+    document.title = route.name() + ' | Nginx UI'
 })
 
 </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>
     <div class="std-curd">
         <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
                 ref="table"
                 v-bind="this.$props"
@@ -14,207 +129,32 @@
                 </template>
             </std-table>
         </a-card>
+
         <a-modal
             class="std-curd-edit-modal"
             :mask="false"
-            :title="data.id ? '编辑 ID: ' + data.id : '添加'"
+            :title="data.id ? $gettext('Modify') : $gettext('Add')"
             :visible="visible"
-            cancel-text="关闭"
-            ok-text="保存"
-            @cancel="visible=false;error={}"
+            :cancel-text="$gettext('Cancel')"
+            :ok-text="$gettext('OK')"
+            @cancel="cancel"
             @ok="ok"
             :width="600"
             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>
     </div>
 </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>

+ 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>
     <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
             :columns="pithyColumns"
             :customRow="row"
@@ -36,319 +235,31 @@
             :scroll="{ x: scrollX }"
         >
             <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>
-                </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>
-            </div>
+            </template>
+
         </a-table>
         <std-pagination :pagination="pagination" @changePage="get_list"/>
     </div>
 </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">
 .ant-table-scroll {
     .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'
 
 export default createGettext({
     availableLanguages: {
-        en: "En",
-        zh_CN: "简",
-        zh_TW: "繁",
+        en: 'En',
+        zh_CN: '简',
+        zh_TW: '繁',
     },
-    defaultLanguage: "en",
+    defaultLanguage: 'en',
     translations: translations,
     silent: true
 })

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
frontend-next/src/language/translations.json


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

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

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

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

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

@@ -1,6 +1,6 @@
 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()
 

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

@@ -5,27 +5,27 @@ function changeCss(css: string, value: string) {
 
 function changeTheme(theme: string) {
     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
     head.appendChild(styleDom)
 }
 
 export const dark_mode = async (enabled: Boolean) => {
     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 {
-        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 {

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

@@ -1,11 +1,11 @@
 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 router from "./routes"
+import router from './routes'
 import 'ant-design-vue/dist/antd.less'
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
-import {useSettingsStore} from "@/pinia/settings"
+import {useSettingsStore} from '@/pinia/settings'
 
 
 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', {
     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', {
     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 {
-    HomeOutlined,
-    UserOutlined,
     CloudOutlined,
-    FileOutlined,
     CodeOutlined,
-    InfoCircleOutlined
+    FileOutlined,
+    HomeOutlined,
+    InfoCircleOutlined,
+    UserOutlined
+} from '@ant-design/icons-vue'
 
-} from "@ant-design/icons-vue"
+const {$gettext} = gettext
 
 export const routes = [
     {
         path: '/',
-        name: $gettext('Home'),
+        name: () => $gettext('Home'),
         component: () => import('@/layouts/BaseLayout.vue'),
         redirect: '/dashboard',
         children: [
             {
                 path: 'dashboard',
                 component: () => import('@/views/dashboard/DashBoard.vue'),
-                name: $gettext('Dashboard'),
+                name: () => $gettext('Dashboard'),
                 meta: {
-                    hiddenHeaderContent: true,
+                    // hiddenHeaderContent: true,
                     icon: HomeOutlined
                 }
             },
             {
                 path: 'user',
-                name: $gettext('Manage Users'),
-                // component: () => import('@/views/user/User.vue'),
+                name: () => $gettext('Manage Users'),
+                component: () => import('@/views/user/User.vue'),
                 meta: {
                     icon: UserOutlined
                 },
             },
             {
                 path: 'domain',
-                name: $gettext('Manage Sites'),
+                name: () => $gettext('Manage Sites'),
                 component: () => import('@/layouts/BaseRouterView.vue'),
                 meta: {
                     icon: CloudOutlined
@@ -49,16 +47,16 @@ export const routes = [
                 redirect: '/domain/list',
                 children: [{
                     path: 'list',
-                    name: $gettext('Sites List'),
-                    // component: () => import('@/views/domain/DomainList.vue'),
+                    name: () => $gettext('Sites List'),
+                    component: () => import('@/views/domain/DomainList.vue'),
                 }, {
                     path: 'add',
-                    name: $gettext('Add Site'),
+                    name: () => $gettext('Add Site'),
                     // component: () => import('@/views/domain/DomainAdd.vue'),
                 }, {
                     path: ':name',
-                    name: $gettext('Edit Site'),
-                    // component: () => import('@/views/domain/DomainEdit.vue'),
+                    name: () => $gettext('Edit Site'),
+                    component: () => import('@/views/domain/DomainEdit.vue'),
                     meta: {
                         hiddenInSidebar: true
                     }
@@ -66,8 +64,8 @@ export const routes = [
             },
             {
                 path: 'config',
-                name: $gettext('Manage Configs'),
-                // component: () => import('@/views/config/Config.vue'),
+                name: () => $gettext('Manage Configs'),
+                component: () => import('@/views/config/Config.vue'),
                 meta: {
                     icon: FileOutlined,
                     hideChildren: true
@@ -75,15 +73,15 @@ export const routes = [
             },
             {
                 path: 'config/:name',
-                name: $gettext('Edit Configuration'),
-                // component: () => import('@/views/config/ConfigEdit.vue'),
+                name: () => $gettext('Edit Configuration'),
+                component: () => import('@/views/config/ConfigEdit.vue'),
                 meta: {
                     hiddenInSidebar: true
                 },
             },
             {
                 path: 'terminal',
-                name: $gettext('Terminal'),
+                name: () => $gettext('Terminal'),
                 component: () => import('@/views/pty/Terminal.vue'),
                 meta: {
                     icon: CodeOutlined
@@ -91,7 +89,7 @@ export const routes = [
             },
             {
                 path: 'about',
-                name: $gettext('About'),
+                name: () => $gettext('About'),
                 component: () => import('@/views/other/About.vue'),
                 meta: {
                     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',
-        name: $gettext('Login'),
+        name: () => $gettext('Login'),
         component: () => import('@/views/other/Login.vue'),
         meta: {noAuth: true}
     },
     {
         path: '/404',
-        name: $gettext('404 Not Found'),
+        name: () => $gettext('404 Not Found'),
         component: () => import('@/views/other/Error.vue'),
         meta: {noAuth: true, status_code: 404, error: 'Not Found'}
     },
     {
         path: '/*',
-        name: $gettext('Not Found'),
+        name: () => $gettext('Not Found'),
         redirect: '/404',
         meta: {noAuth: true}
     }
@@ -132,7 +130,9 @@ const router = createRouter({
 })
 
 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') {
         // 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";
+
 @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>
     <a-card :title="$gettext('Configurations')">
         <std-table
@@ -5,7 +32,6 @@
             :columns="columns"
             :deletable="false"
             :disable_search="true"
-            data_key="configs"
             row-key="name"
             @clickEdit="r => {
                 $router.push({
@@ -16,41 +42,6 @@
     </a-card>
 </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>

+ 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>
     <a-card :title="$gettext('Edit Configuration')">
-        <vue-itextarea v-model="configText"/>
+        <code-editor v-model:content="configText"/>
         <footer-tool-bar>
             <a-space>
                 <a-button @click="$router.go(-1)">
@@ -14,60 +56,6 @@
     </a-card>
 </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>
 .ant-card {
     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 {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()
 

+ 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>
     <div>
         <a-card :bordered="false">
             <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">
                     {{ $gettext('Enabled') }}
                 </a-tag>
@@ -11,7 +119,7 @@
                 </a-tag>
             </template>
             <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">
                     {{ $gettext('Advance Mode') }}
                 </template>
@@ -22,7 +130,7 @@
 
             <transition name="slide-fade">
                 <div v-if="advance_mode" key="advance">
-                    <vue-itextarea v-model="configText"/>
+                    <code-editor v-model:content="configText"/>
                 </div>
 
                 <div class="domain-edit-container" key="basic" v-else>
@@ -30,12 +138,12 @@
                         <a-switch v-model="enabled" @change="checked=>{checked?enable():disable()}"/>
                     </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>
             </transition>
 
@@ -54,128 +162,6 @@
     </div>
 </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>

+ 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>
     <a-card :title="$gettext('Manage Sites')">
         <std-table
-            :api="api"
+            :api="domain"
             :columns="columns"
-            data_key="configs"
             :disable_search="true"
             row-key="name"
             ref="table"
@@ -24,72 +95,6 @@
     </a-card>
 </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>

+ 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">
 import gettext from '@/gettext'
-const {$gettext} = gettext
 import logo from '@/assets/img/logo.png'
 
+const {$gettext} = gettext
+
 const this_year = new Date().getFullYear()
 const version = import.meta.env.VITE_APP_VERSION
 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>Version: {{ version }} ({{ build_id }})</p>
         <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>
         <p>❤️</p>
         <p>Go</p>

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

@@ -72,14 +72,15 @@
             </a-form-item>
         </a-form>
         <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>
     </div>
 
 </template>
 
 <script>
-import SetLanguage from "@/components/SetLanguage/SetLanguage";
+import SetLanguage from '@/components/SetLanguage/SetLanguage'
 
 export default {
     name: 'Login',

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

@@ -1,12 +1,11 @@
 <script setup lang="ts">
 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 {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'
 
 const route = useRoute()
@@ -40,12 +39,11 @@ const {validate, validateInfos} = Form.useForm(modelRef, rulesRef)
 const onSubmit = () => {
     validate().then(() => {
         // modelRef
-        auth.login(modelRef.username, modelRef.password).then(async ()=>{
+        auth.login(modelRef.username, modelRef.password).then(async () => {
             message.success($gettext('Login successful'), 1)
-            const next = (route.query?.next||'').toString() || '/'
+            const next = (route.query?.next || '').toString() || '/'
             await router.push(next)
-        }).
-        catch(e=>{
+        }).catch(e => {
             message.error(e.message)
         })
     })
@@ -88,7 +86,8 @@ const onSubmit = () => {
             </a-form>
             <div class="footer">
                 <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>

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

@@ -2,9 +2,9 @@
 import 'xterm/css/xterm.css'
 import {Terminal} from 'xterm'
 import {FitAddon} from 'xterm-addon-fit'
-import {onMounted, onUnmounted} from "vue"
+import {onMounted, onUnmounted} from 'vue'
 import _ from 'lodash'
-import ws from "@/lib/websocket"
+import ws from '@/lib/websocket'
 
 let term: Terminal | null
 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 gettext from '@/gettext'
+import user from '@/api/user'
+import {datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+
 const {$gettext} = gettext
 
 const columns = [{
@@ -28,33 +27,26 @@ const columns = [{
 }, {
     title: $gettext('Created at'),
     dataIndex: 'created_at',
-    datetime: true,
+    customRender: datetime,
     sorter: true,
     pithy: true
 }, {
     title: $gettext('Updated at'),
     dataIndex: 'updated_at',
-    datetime: true,
+    customRender: datetime,
     sorter: true,
     pithy: true
 }, {
     title: $gettext('Action'),
     dataIndex: 'action'
 }]
-
-export default {
-    name: 'User',
-    components: {StdCurd},
-    data() {
-        return {
-            api: this.$api.user,
-            columns
-        }
-    },
-    methods: {}
-}
 </script>
 
+<template>
+    <std-curd :columns="columns" :api="user" :disable_search="true"/>
+</template>
+
+
 <style scoped>
 
 </style>

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

@@ -1,7 +1,7 @@
 /// <reference types="vite/client" />
 
 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,
         "baseUrl": ".",
         "paths": {
-            "@/*": ["./src/*"]
+            "@/*": [
+                "./src/*"
+            ]
         }
     },
     "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 Components from 'unplugin-vue-components/vite'
 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/
 export default defineConfig({
     resolve: {
         alias: {
-            "@": fileURLToPath(new URL("./src", import.meta.url)),
+            '@': fileURLToPath(new URL('./src', import.meta.url)),
         },
         extensions: [
             '.mjs',
@@ -22,7 +24,7 @@ export default defineConfig({
             '.less'
         ]
     },
-    plugins: [vue(),
+    plugins: [vue(), vueJsx(),
         Components({
             resolvers: [AntDesignVueResolver({importStyle: false})]
         }),

+ 352 - 13
frontend-next/yarn.lock

@@ -2,6 +2,14 @@
 # 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":
   version "6.0.0"
   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"
   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"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
   integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
   dependencies:
     "@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":
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
   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":
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
@@ -48,11 +220,41 @@
     chalk "^2.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"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
   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":
   version "7.18.9"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
@@ -60,12 +262,53 @@
   dependencies:
     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":
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
   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"
   resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
   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"
   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"
   resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
   integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
@@ -180,6 +423,16 @@
   resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
   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":
   version "3.0.1"
   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/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":
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
@@ -339,6 +612,11 @@
     fs-extra "^10.0.0"
     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:
   version "8.8.0"
   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:
     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"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
   integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
@@ -504,6 +782,11 @@ camel-case@^4.1.2:
     pascal-case "^3.1.2"
     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:
   version "3.0.0"
   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"
   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:
   version "2.0.6"
   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"
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
 
-dayjs@^1.10.5:
+dayjs@^1.10.5, dayjs@^1.11.4:
   version "1.11.4"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e"
   integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==
@@ -810,7 +1100,7 @@ debug@^3.2.6:
   dependencies:
     ms "^2.1.1"
 
-debug@^4.3.4:
+debug@^4.1.0, debug@^4.3.4:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
   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"
   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:
   version "3.5.4"
   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"
     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:
   version "4.2.10"
   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"
     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:
   version "0.6.3"
   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"
   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:
   version "2.3.1"
   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==
 
+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:
   version "6.1.0"
   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:
     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:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1955,6 +2265,11 @@ run-parallel@^1.1.9:
   dependencies:
     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":
   version "2.1.2"
   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"
   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:
   version "1.2.1"
   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"
   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:
   version "2.2.2"
   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"
     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:
   version "5.0.1"
   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:
     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:
   version "1.4.1"
   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"
     tslib "^2.3.1"
 
-vue@^3.2.37:
+vue@^3.2.26, vue@^3.2.37:
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
   integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==

+ 2 - 2
server/api/analytic.go

@@ -170,7 +170,7 @@ func GetAnalyticInit(c *gin.Context) {
 		return
 	}
 
-	disk, err := getDiskStat()
+	diskStat, err := getDiskStat()
 
 	if err != nil {
 		log.Println(err)
@@ -200,6 +200,6 @@ func GetAnalyticInit(c *gin.Context) {
 			"reads":  analytic.DiskReadRecord,
 		},
 		"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)
 
 	c.JSON(http.StatusOK, gin.H{
-		"configs": configs,
+		"data": configs,
 	})
 }
 

+ 235 - 235
server/api/domain.go

@@ -1,270 +1,270 @@
 package api
 
 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) {
-    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) {
-    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) {
-    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) {
-    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) {
-    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) {
-    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) {
-    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) {
-    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)
 }

Деякі файли не було показано, через те що забагато файлів було змінено