فهرست منبع

[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)
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است