Przeglądaj źródła

frontend-next base layout

0xJacky 2 lat temu
rodzic
commit
c41a054c8c
100 zmienionych plików z 11134 dodań i 119 usunięć
  1. 1 0
      demo.Dockerfile
  2. 2 0
      frontend-next/.env.development
  3. 2 0
      frontend-next/.env.production
  4. 24 0
      frontend-next/.gitignore
  5. 3 0
      frontend-next/.vscode/extensions.json
  6. 16 0
      frontend-next/README.md
  7. 64 0
      frontend-next/components.d.ts
  8. 5 0
      frontend-next/gettext.config.js
  9. 13 0
      frontend-next/index.html
  10. 44 0
      frontend-next/package.json
  11. BIN
      frontend-next/public/favicon.ico
  12. 1 0
      frontend-next/public/vite.svg
  13. 35 0
      frontend-next/src/App.vue
  14. 23 0
      frontend-next/src/api/auth.ts
  15. 0 0
      frontend-next/src/api/login.ts
  16. BIN
      frontend-next/src/assets/img/logo.png
  17. 49 0
      frontend-next/src/components/Breadcrumb/Breadcrumb.vue
  18. 140 0
      frontend-next/src/components/Chart/CPUChart.vue
  19. 139 0
      frontend-next/src/components/Chart/DiskChart.vue
  20. 37 0
      frontend-next/src/components/Chart/LineChart.vue
  21. 144 0
      frontend-next/src/components/Chart/NetChart.vue
  22. 108 0
      frontend-next/src/components/Chart/RadialBarChart.vue
  23. 52 0
      frontend-next/src/components/FooterToolbar/FooterToolBar.vue
  24. 3 0
      frontend-next/src/components/FooterToolbar/index.js
  25. 43 0
      frontend-next/src/components/Logo/Logo.vue
  26. 204 0
      frontend-next/src/components/PageHeader/PageHeader.vue
  27. 3 0
      frontend-next/src/components/PageHeader/index.js
  28. 36 0
      frontend-next/src/components/SetLanguage/SetLanguage.vue
  29. 220 0
      frontend-next/src/components/StdDataDisplay/StdCurd.vue
  30. 51 0
      frontend-next/src/components/StdDataDisplay/StdPagination.vue
  31. 374 0
      frontend-next/src/components/StdDataDisplay/StdTable.vue
  32. 53 0
      frontend-next/src/components/StdDataEntry/StdCheckGroup.vue
  33. 65 0
      frontend-next/src/components/StdDataEntry/StdCheckTag.vue
  34. 237 0
      frontend-next/src/components/StdDataEntry/StdDataEntry.vue
  35. 52 0
      frontend-next/src/components/StdDataEntry/StdDatePicker.vue
  36. 75 0
      frontend-next/src/components/StdDataEntry/StdMultiCheckTag.vue
  37. 153 0
      frontend-next/src/components/StdDataEntry/StdMultiFilesUpload.vue
  38. 49 0
      frontend-next/src/components/StdDataEntry/StdRadioGroup.vue
  39. 47 0
      frontend-next/src/components/StdDataEntry/StdSelectOption.vue
  40. 156 0
      frontend-next/src/components/StdDataEntry/StdSelector.vue
  41. 105 0
      frontend-next/src/components/StdDataEntry/StdSingleFileUpload.vue
  42. 75 0
      frontend-next/src/components/StdDataEntry/StdTransfer.vue
  43. 266 0
      frontend-next/src/components/StdDataEntry/StdUpload.vue
  44. 1 0
      frontend-next/src/dark.less
  45. 12 0
      frontend-next/src/gettext.ts
  46. 1 0
      frontend-next/src/language/LINGUAS
  47. 508 0
      frontend-next/src/language/en/app.po
  48. 533 0
      frontend-next/src/language/messages.pot
  49. 0 0
      frontend-next/src/language/translations.json
  50. 639 0
      frontend-next/src/language/zh_CN/app.po
  51. 642 0
      frontend-next/src/language/zh_TW/app.po
  52. 229 0
      frontend-next/src/layouts/BaseLayout.vue
  53. 13 0
      frontend-next/src/layouts/BaseRouterView.vue
  54. 22 0
      frontend-next/src/layouts/FooterLayout.vue
  55. 83 0
      frontend-next/src/layouts/HeaderLayout.vue
  56. 33 0
      frontend-next/src/layouts/Loading.vue
  57. 124 0
      frontend-next/src/layouts/SideBar.vue
  58. 52 0
      frontend-next/src/lib/http/index.ts
  59. 24 0
      frontend-next/src/main.ts
  60. 16 0
      frontend-next/src/pinia/settings.ts
  61. 20 0
      frontend-next/src/pinia/user.ts
  62. 163 0
      frontend-next/src/routes/index.ts
  63. 2 0
      frontend-next/src/style.less
  64. 56 0
      frontend-next/src/views/config/Config.vue
  65. 78 0
      frontend-next/src/views/config/ConfigEdit.vue
  66. 327 0
      frontend-next/src/views/dashboard/DashBoard.vue
  67. 159 0
      frontend-next/src/views/domain/DomainAdd.vue
  68. 223 0
      frontend-next/src/views/domain/DomainEdit.vue
  69. 95 0
      frontend-next/src/views/domain/DomainList.vue
  70. 44 0
      frontend-next/src/views/domain/cert/Cert.vue
  71. 74 0
      frontend-next/src/views/domain/cert/CertInfo.vue
  72. 169 0
      frontend-next/src/views/domain/cert/IssueCert.vue
  73. 37 0
      frontend-next/src/views/domain/methods.js
  74. 78 0
      frontend-next/src/views/domain/ngx_conf/LocationEditor.vue
  75. 181 0
      frontend-next/src/views/domain/ngx_conf/NgxConfigEditor.vue
  76. 74 0
      frontend-next/src/views/domain/ngx_conf/directive/DirectiveAdd.vue
  77. 92 0
      frontend-next/src/views/domain/ngx_conf/directive/DirectiveEditor.vue
  78. 1 0
      frontend-next/src/views/domain/ngx_conf/ngx_constant.js
  79. 48 0
      frontend-next/src/views/other/About.vue
  80. 62 0
      frontend-next/src/views/other/Error.vue
  81. 145 0
      frontend-next/src/views/other/Install.vue
  82. 133 0
      frontend-next/src/views/other/Login.vue
  83. 110 0
      frontend-next/src/views/pty/Terminal.vue
  84. 60 0
      frontend-next/src/views/user/User.vue
  85. 7 0
      frontend-next/src/vite-env.d.ts
  86. 34 0
      frontend-next/tsconfig.json
  87. 9 0
      frontend-next/tsconfig.node.json
  88. 101 0
      frontend-next/vite.config.ts
  89. 2280 0
      frontend-next/yarn.lock
  90. 2 2
      frontend/.env.development
  91. 19 0
      frontend/conversion.log
  92. 24 0
      frontend/index.html
  93. 87 76
      frontend/package.json
  94. 2 2
      frontend/src/lazy.js
  95. 1 1
      frontend/src/lib/http/index.js
  96. 3 3
      frontend/src/lib/utils/index.js
  97. 16 17
      frontend/src/locale/en/LC_MESSAGES/app.po
  98. 16 17
      frontend/src/locale/zh_CN/LC_MESSAGES/app.po
  99. 0 0
      frontend/src/translations.json
  100. 1 1
      frontend/src/views/config/Config.vue

+ 1 - 0
demo.Dockerfile

@@ -3,6 +3,7 @@ FROM --platform=linux/amd64 uozi/nginx-ui-base:latest
 WORKDIR /app
 EXPOSE 80
 
+COPY resources/demo/ojbk.me /etc/nginx/sites-available/ojbk.me
 COPY resources/demo/app.ini /etc/nginx-ui/app.ini
 COPY resources/demo/demo.db /etc/nginx-ui/database.db
 COPY resources/docker/nginx.conf /etc/nginx/nginx.conf

+ 2 - 0
frontend-next/.env.development

@@ -0,0 +1,2 @@
+VITE_API_ROOT = /api
+VITE_API_WSS_ROOT = wss://nginx.jackyu.cn/api

+ 2 - 0
frontend-next/.env.production

@@ -0,0 +1,2 @@
+VUE_APP_API_ROOT = /api
+VUE_APP_API_WSS_ROOT = /api

+ 24 - 0
frontend-next/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

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

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

+ 16 - 0
frontend-next/README.md

@@ -0,0 +1,16 @@
+# 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.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
+
+## 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:
+
+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).

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

@@ -0,0 +1,64 @@
+// generated by unplugin-vue-components
+// We suggest you to commit this file into source control
+// Read more: https://github.com/vuejs/core/pull/3399
+import '@vue/runtime-core'
+
+export {}
+
+declare module '@vue/runtime-core' {
+  export interface GlobalComponents {
+    AAvatar: typeof import('ant-design-vue/es')['Avatar']
+    ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
+    ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
+    AButton: typeof import('ant-design-vue/es')['Button']
+    ACard: typeof import('ant-design-vue/es')['Card']
+    ACol: typeof import('ant-design-vue/es')['Col']
+    AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    ADrawer: typeof import('ant-design-vue/es')['Drawer']
+    AForm: typeof import('ant-design-vue/es')['Form']
+    AFormItem: typeof import('ant-design-vue/es')['FormItem']
+    AInput: typeof import('ant-design-vue/es')['Input']
+    AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
+    ALayout: typeof import('ant-design-vue/es')['Layout']
+    ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
+    ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
+    ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
+    ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
+    AMenu: typeof import('ant-design-vue/es')['Menu']
+    AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
+    ARow: typeof import('ant-design-vue/es')['Row']
+    ASelect: typeof import('ant-design-vue/es')['Select']
+    ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
+    AStatistic: typeof import('ant-design-vue/es')['Statistic']
+    ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
+    Breadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
+    CPUChart: typeof import('./src/components/Chart/CPUChart.vue')['default']
+    DiskChart: typeof import('./src/components/Chart/DiskChart.vue')['default']
+    DynamicIcon: typeof import('./src/components/DynamicIcon/DynamicIcon.vue')['default']
+    FooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    LineChart: typeof import('./src/components/Chart/LineChart.vue')['default']
+    Logo: typeof import('./src/components/Logo/Logo.vue')['default']
+    NetChart: typeof import('./src/components/Chart/NetChart.vue')['default']
+    PageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']
+    RadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    SetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
+    StdCheckGroup: typeof import('./src/components/StdDataEntry/StdCheckGroup.vue')['default']
+    StdCheckTag: typeof import('./src/components/StdDataEntry/StdCheckTag.vue')['default']
+    StdCurd: typeof import('./src/components/StdDataDisplay/StdCurd.vue')['default']
+    StdDataEntry: typeof import('./src/components/StdDataEntry/StdDataEntry.vue')['default']
+    StdDatePicker: typeof import('./src/components/StdDataEntry/StdDatePicker.vue')['default']
+    StdMultiCheckTag: typeof import('./src/components/StdDataEntry/StdMultiCheckTag.vue')['default']
+    StdMultiFilesUpload: typeof import('./src/components/StdDataEntry/StdMultiFilesUpload.vue')['default']
+    StdPagination: typeof import('./src/components/StdDataDisplay/StdPagination.vue')['default']
+    StdRadioGroup: typeof import('./src/components/StdDataEntry/StdRadioGroup.vue')['default']
+    StdSelectOption: typeof import('./src/components/StdDataEntry/StdSelectOption.vue')['default']
+    StdSelector: typeof import('./src/components/StdDataEntry/StdSelector.vue')['default']
+    StdSingleFileUpload: typeof import('./src/components/StdDataEntry/StdSingleFileUpload.vue')['default']
+    StdTable: typeof import('./src/components/StdDataDisplay/StdTable.vue')['default']
+    StdTransfer: typeof import('./src/components/StdDataEntry/StdTransfer.vue')['default']
+    StdUpload: typeof import('./src/components/StdDataEntry/StdUpload.vue')['default']
+  }
+}

+ 5 - 0
frontend-next/gettext.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+    output: {
+        locales: ['en', 'zh_CN', 'zh_TW'],
+    },
+}

+ 13 - 0
frontend-next/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8"/>
+    <link href="/favicon.ico" rel="icon">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <title><%- title %></title>
+</head>
+<body>
+<div id="app"></div>
+<script type="module" src="/src/main.ts"></script>
+</body>
+</html>

+ 44 - 0
frontend-next/package.json

@@ -0,0 +1,44 @@
+{
+    "name": "nginx-ui-frontend-next",
+    "private": true,
+    "version": "0.0.0",
+    "type": "commonjs",
+    "scripts": {
+        "dev": "vite",
+        "build": "vue-tsc --noEmit && vite build",
+        "preview": "vite preview",
+        "gettext:extract": "vue-gettext-extract",
+        "gettext:compile": "vue-gettext-compile"
+    },
+    "dependencies": {
+        "@ant-design/icons-vue": "^6.1.0",
+        "ant-design-vue": "^3.2.10",
+        "axios": "^0.27.2",
+        "lodash": "^4.17.21",
+        "moment": "^2.29.4",
+        "path": "^0.12.7",
+        "pinia": "^2.0.17",
+        "pinia-plugin-persistedstate": "^1.6.3",
+        "reconnecting-websocket": "^4.4.0",
+        "vue": "^3.2.37",
+        "vue-apexcharts": "^1.6.2",
+        "vue-chartjs": "^4.1.1",
+        "vue-router": "4",
+        "vue3-gettext": "^2.3.0",
+        "vuex": "^4.0.2",
+        "xterm": "^4.19.0",
+        "xterm-addon-attach": "^0.6.0",
+        "xterm-addon-fit": "^0.5.0"
+    },
+    "devDependencies": {
+        "@types/lodash": "^4.14.182",
+        "@vitejs/plugin-vue": "^3.0.0",
+        "@zougt/vite-plugin-theme-preprocessor": "^1.4.5",
+        "less": "^4.1.3",
+        "typescript": "^4.6.4",
+        "unplugin-vue-components": "^0.21.2",
+        "vite": "^3.0.0",
+        "vite-plugin-html": "^3.2.0",
+        "vue-tsc": "^0.38.4"
+    }
+}

BIN
frontend-next/public/favicon.ico


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

@@ -0,0 +1 @@
+<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>

+ 35 - 0
frontend-next/src/App.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+// This starter template is using Vue 3 <script setup> SFCs
+// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
+import {toggleTheme} from '@zougt/vite-plugin-theme-preprocessor/dist/browser-utils.js'
+
+let media = window.matchMedia('(prefers-color-scheme: dark)')
+const callback = (media: { matches: any; }) => {
+    if (media.matches) {
+        toggleTheme({
+            scopeName: 'theme-dark'
+        })
+    } else {
+        toggleTheme({
+            scopeName: 'theme-default'
+        })
+    }
+}
+callback(media)
+if (typeof media.addEventListener === 'function') {
+    media.addEventListener('change', callback)
+} else if (typeof media.addListener === 'function') {
+    media.addListener(callback)
+}
+
+</script>
+
+<template>
+    <router-view/>
+</template>
+
+<style lang="less" scoped>
+#app {
+    height: 100%;
+}
+</style>

+ 23 - 0
frontend-next/src/api/auth.ts

@@ -0,0 +1,23 @@
+import http from "@/lib/http"
+import {useUserStore} from "@/pinia/user"
+
+const user = useUserStore()
+const {login, logout} = user
+
+const auth = {
+    async login(name: string, password: string) {
+        return http.post('/login', {
+            name: name,
+            password: password
+        }).then(r => {
+            login(r.data.token)
+        })
+    },
+    logout() {
+        return http.delete('/logout').then(async () => {
+            logout()
+        })
+    }
+}
+
+export default auth

+ 0 - 0
frontend-next/src/api/login.ts


BIN
frontend-next/src/assets/img/logo.png


+ 49 - 0
frontend-next/src/components/Breadcrumb/Breadcrumb.vue

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

+ 140 - 0
frontend-next/src/components/Chart/CPUChart.vue

@@ -0,0 +1,140 @@
+<template>
+    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+const fontColor = () => {
+    return window.matchMedia('(prefers-color-scheme: dark)').matches ? '#b4b4b4' : undefined
+}
+export default {
+    name: 'CPUChart',
+    props: {
+        series: Array
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    mounted() {
+        let media = window.matchMedia('(prefers-color-scheme: dark)')
+        let callback = () => {
+            this.chartOptions.xaxis = {
+                type: 'datetime',
+                labels: {
+                    datetimeUTC: false,
+                    style: {
+                        colors: fontColor()
+                    }
+                }
+            }
+            this.chartOptions.yaxis = {
+                max: 100,
+                tickAmount: 4,
+                min: 0,
+                labels: {
+                    style: {
+                        colors: fontColor()
+                    }
+                }
+            }
+            this.chartOptions.legend = {
+                labels: {
+                    colors: fontColor()
+                },
+                onItemClick: {
+                    toggleDataSeries: false
+                },
+                onItemHover: {
+                    highlightDataSeries: false
+                },
+            }
+            this.$refs.chart.updateOptions(this.chartOptions)
+        }
+        if (typeof media.addEventListener === 'function') {
+            media.addEventListener('change', callback)
+        } else if (typeof media.addListener === 'function') {
+            media.addListener(callback)
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'area',
+                    zoom: {
+                        enabled: false
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                    toolbar: {
+                        show: false
+                    },
+                },
+                colors: ['#ff6385', '#36a3eb'],
+                fill: {
+                    // type: ['solid', 'gradient'],
+                    gradient: {
+                        shade: 'light'
+                    }
+                    //colors:  ['#ff6385', '#36a3eb'],
+                },
+                dataLabels: {
+                    enabled: false
+                },
+                stroke: {
+                    curve: 'smooth',
+                    width: 0,
+                },
+                xaxis: {
+                    type: 'datetime',
+                    labels: {
+                        datetimeUTC: false,
+                        style: {
+                            colors: fontColor()
+                        }
+                    }
+                },
+                tooltip: {
+                    enabled: false
+                },
+                yaxis: {
+                    max: 100,
+                    tickAmount: 4,
+                    min: 0,
+                    labels: {
+                        style: {
+                            colors: fontColor()
+                        }
+                    }
+                },
+                legend: {
+                    labels: {
+                        colors: fontColor()
+                    },
+                    onItemClick: {
+                        toggleDataSeries: false
+                    },
+                    onItemHover: {
+                        highlightDataSeries: false
+                    },
+                }
+            },
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 139 - 0
frontend-next/src/components/Chart/DiskChart.vue

@@ -0,0 +1,139 @@
+<template>
+    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+
+const fontColor = () => {
+    return window.matchMedia('(prefers-color-scheme: dark)').matches ? '#b4b4b4' : null
+}
+export default {
+    name: 'DiskChart',
+    props: {
+        series: Array
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        },
+    },
+    mounted() {
+        let media = window.matchMedia('(prefers-color-scheme: dark)')
+        let callback = () => {
+            this.chartOptions.xaxis = {
+                type: 'datetime',
+                    labels: {
+                    datetimeUTC: false,
+                        style: {
+                        colors: fontColor()
+                    }
+                }
+            }
+            this.chartOptions.yaxis = {
+                tickAmount: 3,
+                    min: 0,
+                    labels: {
+                    style: {
+                        colors: fontColor()
+                    }
+                }
+            }
+            this.chartOptions.legend = {
+                labels: {
+                    colors: fontColor()
+                },
+                onItemClick: {
+                    toggleDataSeries: false
+                },
+                onItemHover: {
+                    highlightDataSeries: false
+                },
+            }
+            this.$refs.chart.updateOptions(this.chartOptions)
+        }
+        if (typeof media.addEventListener === 'function') {
+            media.addEventListener('change', callback)
+        } else if (typeof media.addListener === 'function') {
+            media.addListener(callback)
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'area',
+                    zoom: {
+                        enabled: false
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                    toolbar: {
+                        show: false
+                    },
+                },
+                colors: ['#ff6385', '#36a3eb'],
+                fill: {
+                    // type: ['solid', 'gradient'],
+                    gradient: {
+                        shade: 'light'
+                    }
+                    //colors:  ['#ff6385', '#36a3eb'],
+                },
+                dataLabels: {
+                    enabled: false
+                },
+                stroke: {
+                    curve: 'smooth',
+                    width: 0,
+                },
+                xaxis: {
+                    type: 'datetime',
+                    labels: {
+                        datetimeUTC: false,
+                        style: {
+                            colors: fontColor()
+                        }
+                    }
+                },
+                tooltip: {
+                    enabled: false
+                },
+                yaxis: {
+                    tickAmount: 3,
+                    min: 0,
+                    labels: {
+                        style: {
+                            colors: fontColor()
+                        }
+                    }
+                },
+                legend: {
+                    labels: {
+                        colors: fontColor()
+                    },
+                    onItemClick: {
+                        toggleDataSeries: false
+                    },
+                    onItemHover: {
+                        highlightDataSeries: false
+                    },
+                }
+            },
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 37 - 0
frontend-next/src/components/Chart/LineChart.vue

@@ -0,0 +1,37 @@
+<script>
+import {Line, mixins} from 'vue-chartjs'
+
+const {reactiveProp} = mixins
+
+export default {
+    name: 'LineChart',
+    extends: Line,
+    mixins: [reactiveProp],
+    props: ['options'],
+    data() {
+        return {
+            updating: false
+        }
+    },
+    mounted() {
+        this.renderChart(this.chartData, this.options)
+    },
+    watch: {
+        chartData: {
+            deep: true,
+            handler() {
+                if (!this.updating && this.$data && this.$data._chart) {
+                    // Update the chart
+                    this.updating = true
+                    this.$data._chart.update()
+                    this.$nextTick(() => this.updating = false)
+                }
+            }
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+
+</style>

+ 144 - 0
frontend-next/src/components/Chart/NetChart.vue

@@ -0,0 +1,144 @@
+<template>
+    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+const fontColor = () => {
+    return window.matchMedia('(prefers-color-scheme: dark)').matches ? '#b4b4b4' : undefined
+}
+export default {
+    name: 'NetChart',
+    props: {
+        series: Array
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    mounted() {
+        let media = window.matchMedia('(prefers-color-scheme: dark)')
+        let callback = () => {
+            this.chartOptions.xaxis = {
+                type: 'datetime',
+                labels: {
+                    datetimeUTC: false,
+                    style: {
+                        colors: fontColor()
+                    }
+                }
+            }
+            this.chartOptions.yaxis = {
+                tickAmount: 3,
+                min: 0,
+                labels: {
+                    style: {
+                        colors: fontColor()
+                    },
+                    formatter: (bytes) => {
+                        return this.bytesToSize(bytes) + '/s'
+                    }
+                }
+            }
+            this.chartOptions.legend = {
+                labels: {
+                    colors: fontColor()
+                },
+                onItemClick: {
+                    toggleDataSeries: false
+                },
+                onItemHover: {
+                    highlightDataSeries: false
+                },
+            }
+            this.$refs.chart.updateOptions(this.chartOptions)
+        }
+        if (typeof media.addEventListener === 'function') {
+            media.addEventListener('change', callback)
+        } else if (typeof media.addListener === 'function') {
+            media.addListener(callback)
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'area',
+                    zoom: {
+                        enabled: false
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                    toolbar: {
+                        show: false
+                    },
+                },
+                colors: ['#ff6385', '#36a3eb'],
+                fill: {
+                    // type: ['solid', 'gradient'],
+                    gradient: {
+                        shade: 'light'
+                    }
+                    //colors:  ['#ff6385', '#36a3eb'],
+                },
+                dataLabels: {
+                    enabled: false
+                },
+                stroke: {
+                    curve: 'smooth',
+                    width: 0,
+                },
+                xaxis: {
+                    type: 'datetime',
+                    labels: {
+                        datetimeUTC: false,
+                        style: {
+                            colors: fontColor()
+                        }
+                    }
+                },
+                tooltip: {
+                    enabled: false
+                },
+                yaxis: {
+                    tickAmount: 3,
+                    min: 0,
+                    labels: {
+                        style: {
+                            colors: fontColor()
+                        },
+                        formatter: (bytes) => {
+                            return this.bytesToSize(bytes) + '/s'
+                        }
+                    }
+                },
+                legend: {
+                    labels: {
+                        colors: fontColor()
+                    },
+                    onItemClick: {
+                        toggleDataSeries: false
+                    },
+                    onItemHover: {
+                        highlightDataSeries: false
+                    },
+                }
+            },
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 108 - 0
frontend-next/src/components/Chart/RadialBarChart.vue

@@ -0,0 +1,108 @@
+<template>
+    <div class="container">
+        <p class="text">{{ centerText }}</p>
+        <p class="bottom_text">{{ bottomText }}</p>
+        <apexchart class="radialBar" type="radialBar" height="205" :options="chartOptions" :series="series" ref="chart"/>
+    </div>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+export default {
+    name: 'RadialBarChart',
+    props: {
+        series: Array,
+        centerText: String,
+        colors: String,
+        name: String,
+        bottomText: String,
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'radialBar',
+                    offsetY: 0
+                },
+                plotOptions: {
+                    radialBar: {
+                        startAngle: -135,
+                        endAngle: 135,
+                        dataLabels: {
+                            name: {
+                                fontSize: '14px',
+                                color: this.colors,
+                                offsetY: 36
+                            },
+                            value: {
+                                offsetY: 50,
+                                fontSize: '14px',
+                                color: undefined,
+                                formatter: () => {return ''}
+                            }
+                        }
+                    }
+                },
+                fill: {
+                    colors: this.colors
+                },
+                labels: [this.name],
+                states: {
+                    hover: {
+                        filter: {
+                            type: 'none'
+                        }
+                    },
+                    active: {
+                        filter: {
+                            type: 'none'
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+    position: relative;
+    margin: 0 auto;
+    height: 112px!important;
+    .radialBar {
+        position: absolute;
+        top: -30px;
+        @media(max-width: 768px) and (min-width: 290px) {
+            left: 50%;
+            transform: translateX(-50%);
+        }
+    }
+    .text {
+        position: absolute;
+        top: calc(50% - 5px);
+        width: 100%;
+        text-align: center;
+    }
+    .bottom_text {
+        position: absolute;
+        top: calc(106px);
+        font-weight: 600;
+        width: 100%;
+        text-align: center;
+    }
+}
+</style>

+ 52 - 0
frontend-next/src/components/FooterToolbar/FooterToolBar.vue

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

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

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

+ 43 - 0
frontend-next/src/components/Logo/Logo.vue

@@ -0,0 +1,43 @@
+<script setup lang="ts">
+import logo from '@/assets/img/logo.png'
+</script>
+
+<template>
+    <div class="logo">
+        <img :src="logo" alt="logo"/>
+        <p class="text">Nginx UI</p>
+        <div class="clear"></div>
+    </div>
+</template>
+
+<style lang="less" scoped>
+.logo {
+    padding: 8px 25px;
+    -webkit-box-shadow: 1px 1px 0 0 #e8e8e8;
+    box-shadow: 1px 1px 0 0 #e8e8e8;
+    transition: all 0.3s;
+    height: 64px;
+    width: 100%;
+    overflow: hidden;
+    display: inline-block;
+    background-color: #ffffff;
+    @media (prefers-color-scheme: dark) {
+        background-color: transparent;
+        -webkit-box-shadow: 1px 1px 0 0 #404040;
+        box-shadow: 1px 1px 0 0 #404040;
+    }
+
+    img {
+        height: 46px;
+        float: left;
+    }
+
+    .text {
+        float: left;
+        font-size: 22px;
+        line-height: 48px;
+        height: 48px;
+        display: inline-block;
+    }
+}
+</style>

+ 204 - 0
frontend-next/src/components/PageHeader/PageHeader.vue

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

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

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

+ 36 - 0
frontend-next/src/components/SetLanguage/SetLanguage.vue

@@ -0,0 +1,36 @@
+<script setup lang="ts">
+import gettext from "@/gettext"
+
+
+import {ref, watch, nextTick} from "vue"
+
+import {useSettingsStore} from "@/pinia/settings"
+const settings = useSettingsStore()
+
+
+const current = ref(gettext.current)
+
+const languageAvailable = gettext.available
+watch(current, (v) => {
+    settings.set_language(v)
+    gettext.current = v
+    // nextTick(() => {
+    //     location.reload()
+    // })
+})
+
+</script>
+
+<template>
+    <div>
+        <a-select v-model:value="current" size="small" style="width: 60px">
+            <a-select-option v-for="(language, key) in languageAvailable" :value="key" :key="key">
+                {{ language }}
+            </a-select-option>
+        </a-select>
+    </div>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 220 - 0
frontend-next/src/components/StdDataDisplay/StdCurd.vue

@@ -0,0 +1,220 @@
+<template>
+    <div class="std-curd">
+        <a-card :title="title">
+            <a v-if="!disable_add" slot="extra" @click="add">添加</a>
+            <std-table
+                ref="table"
+                v-bind="this.$props"
+                @clickEdit="edit"
+                @selected="onSelect"
+                :key="update"
+            >
+                <template v-slot:actions="slotProps">
+                    <slot name="actions" :actions="slotProps.record"/>
+                </template>
+            </std-table>
+        </a-card>
+        <a-modal
+            class="std-curd-edit-modal"
+            :mask="false"
+            :title="data.id ? '编辑 ID: ' + data.id : '添加'"
+            :visible="visible"
+            cancel-text="关闭"
+            ok-text="保存"
+            @cancel="visible=false;error={}"
+            @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"/>
+        </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>

+ 51 - 0
frontend-next/src/components/StdDataDisplay/StdPagination.vue

@@ -0,0 +1,51 @@
+<template>
+    <div v-if="Object.keys(pagination).length !== 0">
+        <a-pagination
+            :current="pagination.current_page"
+            :hideOnSinglePage="true"
+            :pageSize="pagination.per_page"
+            :size="size"
+            :total="pagination.total"
+            :show-total="(total, range) => `当前显示${range[0]}-${range[1]}条数据,共${total}条数据`"
+            class="pagination"
+            @change="changePage"
+        />
+        <div class="clear"></div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'StdPagination',
+    props: {
+        pagination: Object,
+        size: {
+            default: ''
+        }
+    },
+    methods: {
+        changePage(num) {
+            return this.$emit('changePage', num)
+        }
+    }
+}
+</script>
+
+<style lang="less">
+.ant-pagination-total-text {
+    @media (max-width: 450px) {
+        display: block;
+    }
+}
+</style>
+
+<style lang="less" scoped>
+.pagination {
+    padding: 10px 0 0 0;
+    float: right;
+    @media (max-width: 450px) {
+        float: unset;
+        text-align: center;
+    }
+}
+</style>

+ 374 - 0
frontend-next/src/components/StdDataDisplay/StdTable.vue

@@ -0,0 +1,374 @@
+<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>
+        <a-table
+            :columns="pithyColumns"
+            :customRow="row"
+            :data-source="data_source"
+            :loading="loading"
+            :pagination="false"
+            :row-key="rowKey"
+            :rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange,
+            onSelect: onSelect, type: selectionType,}"
+            @change="stdChange"
+            :scroll="{ x: scrollX }"
+        >
+            <template
+                v-for="c in pithyColumns"
+                :slot="c.scopedSlots.customRender"
+                slot-scope="text, record"
+            >
+                <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 }}
+                    </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>
+                </template>
+            </div>
+        </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 {
+        overflow-x: auto !important;
+    }
+}
+</style>
+
+<style lang="less" scoped>
+.ant-form {
+    margin: 10px 0 20px 0;
+}
+
+.ant-slider {
+    min-width: 90px;
+}
+
+.std-table {
+    .ant-table-wrapper {
+        // overflow-x: scroll;
+    }
+}
+</style>

+ 53 - 0
frontend-next/src/components/StdDataEntry/StdCheckGroup.vue

@@ -0,0 +1,53 @@
+<template>
+    <div>
+        <a-checkbox-group v-model="checkedList" :options="options" @change="onChange"/>
+        <template v-if="allowOther&&checkedList.indexOf('其他')>0">
+            <a-form-item label="其他">
+                <a-input v-model="other" @change="onChangeOther"/>
+            </a-form-item>
+        </template>
+    </div>
+</template>
+<script>
+export default {
+    name: 'StdCheckGroup',
+    props: {
+        options: Array,
+        allowOther: Boolean,
+        data: {
+            type: Object,
+            default() {
+                return {
+                    checkedList: [],
+                    other: ''
+                }
+            }
+        }
+    },
+    model: {
+        prop: 'data',
+        event: 'changeData'
+    },
+    watch: {
+        data() {
+            this.checkedList = this.data.checkedList
+            this.other = this.data.other
+        }
+    },
+    data() {
+        return {
+            checkedList: this.data.checkedList,
+            other: this.data.other
+        }
+    },
+    methods: {
+        onChange(checkedList) {
+            this.checkedList = checkedList
+            this.$emit('changeData', this.$data)
+        },
+        onChangeOther() {
+            this.$emit('changeData', this.$data)
+        }
+    },
+}
+</script>

+ 65 - 0
frontend-next/src/components/StdDataEntry/StdCheckTag.vue

@@ -0,0 +1,65 @@
+<template>
+    <div>
+        <template v-for="(v,k) in options">
+            <a-checkable-tag
+                :key="k"
+                :checked="selectedTag === k"
+                @change="() => handleChange(k)"
+            >
+                {{ v }}
+            </a-checkable-tag>
+        </template>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'StdCheckTag',
+    data() {
+        return {
+            selectedTag: '',
+        }
+    },
+    props: {
+        disabled: [Boolean],
+        value: [Number, String, Boolean],
+        options: [Array, Object],
+        keyType: {
+            type: String,
+            default() {
+                return 'int'
+            }
+        }
+    },
+    model: {
+        prop: 'value',
+        event: 'change'
+    },
+    methods: {
+        handleChange(tag) {
+            if (!this.disabled) {
+                this.selectedTag = tag
+                this.$emit('change', isNaN(parseInt(tag)) || this.keyType === 'string' ? tag : parseInt(tag))
+            }
+        }
+    },
+    watch: {
+        value() {
+            this.selectedTag = this.value != null ? this.value.toString() : null
+        }
+    },
+    created() {
+        this.selectedTag = this.value != null ? this.value.toString() : null
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-tag {
+    background-color: rgba(0, 0, 0, 0.05);
+}
+
+.ant-tag-checkable-checked {
+    background-color: #1890ff;
+}
+</style>

+ 237 - 0
frontend-next/src/components/StdDataEntry/StdDataEntry.vue

@@ -0,0 +1,237 @@
+<template>
+    <a-form :layout="layout" class="std-data-entry">
+        <a-form-item
+            v-for="d in M_dataList" :key="d.dataIndex" :help="error[d.dataIndex] ? error[d.dataIndex].toString() : null"
+            :label="d.title"
+            :labelCol="d.edit.labelCol"
+            :validate-status="error[d.dataIndex] ? 'error' :'success'"
+            :wrapperCol="d.edit.wrapperCol"
+        >
+            <p v-if="d.description" v-html="d.description+'<br/>'"/>
+            <a-input
+                v-if="d.edit.type==='input'"
+                v-model="dataSource[d.dataIndex]"
+                :placeholder="getInputPlaceholder(d, dataSource)"
+            />
+            <a-textarea v-else-if="d.edit.type==='textarea'" v-model="dataSource[d.dataIndex]"
+                        :rows="d.edit.row?d.edit.row:5"/>
+            <std-select-option
+                v-else-if="d.edit.type==='select'"
+                v-model="temp[d.dataIndex]"
+                :options="d.mask"
+                :key-type="d.edit.key_type ? d.edit.key_type : 'int'"
+                style="min-width: 120px"
+            />
+
+            <std-check-tag
+                v-else-if="d.edit.type==='check-tag'"
+                v-model="temp[d.dataIndex]"
+                :options="d.mask"
+            />
+
+            <std-multi-check-tag
+                v-else-if="d.edit.type==='multi-check-tag'"
+                v-model="temp[d.dataIndex]"
+                :data-object="temp"
+                :options="d.mask"
+            />
+
+            <std-selector
+                v-else-if="d.edit.type==='selector'" v-model="temp[d.dataIndex]" :api="d.edit.api"
+                :columns="d.edit.columns"
+                :data_key="d.edit.data_key"
+                :disable_search="d.edit.disable_search" :pagination_method="d.edit.pagination_method"
+                :record-value-index="d.edit.recordValueIndex" :value="fn(temp, d.edit.valueIndex)"
+                :get_params="get_params_fn(d)"
+                :description="d.edit.description"
+                selection-type="radio"
+            />
+
+            <a-input-number v-else-if="d.edit.type==='number'" v-model="temp[d.dataIndex]"
+                            :min="d.edit.min" :step="d.edit.step" :max="d.edit.max"
+            />
+
+            <std-upload v-else-if="d.edit.type==='upload'" :id="temp.id?temp.id:null" :ref="'std_upload_'+d.dataIndex"
+                        v-model="temp[d.dataIndex]" :api="d.edit.api"
+                        :api_delete="d.edit.api_delete"
+                        :list="temp[d.dataIndex]"
+                        :crop="d.edit.crop"
+                        :auto-upload="d.edit.auto_upload"
+                        :crop-options="d.edit.cropOptions" :type="d.edit.upload_type ? d.edit.upload_type : 'img'"
+                        @uploaded="url => {$emit('uploaded', url)}"
+            />
+
+            <std-date-picker v-else-if="d.edit.type==='date_picker'" v-model="temp[d.dataIndex]"
+                             :show-time="d.edit.showTime"/>
+
+            <a-slider
+                v-else-if="d.edit.type==='slider'"
+                v-model="temp[d.dataIndex]"
+                :marks="d.mask"
+                :max="d.edit.max"
+                :min="d.edit.min"
+            />
+
+            <a-switch
+                v-else-if="d.edit.type==='switch'"
+                v-model="temp[d.dataIndex]"
+            />
+
+            <a-checkbox
+                v-else-if="d.edit.type==='checkbox'"
+                v-model="temp[d.dataIndex]"
+            >
+                {{ d.text }}
+            </a-checkbox>
+
+            <std-check-group
+                v-else-if="d.edit.type==='check-group'"
+                v-model="temp[d.dataIndex]"
+                :options="d.options"
+                :allow-other="d.edit.allow_other"
+            />
+
+            <std-radio-group
+                v-else-if="d.edit.type==='radio-group'"
+                v-model="temp[d.dataIndex]"
+                :options="d.options"
+                :key-type="d.edit.key_type"
+            />
+
+            <std-transfer
+                v-else-if="d.edit.type==='transfer'"
+                v-model="temp[d.dataIndex]"
+                :api="d.edit.api"
+                :data-key="d.edit.dataKey"
+            />
+
+            <p v-else-if="d.edit.type==='readonly'">
+                {{ d.mask ? d.mask[fn(temp, d.dataIndex)] : fn(temp, d.dataIndex) }}
+            </p>
+
+            <p v-else>{{ 'edit.type 参数非法 ' + d.edit.type }}</p>
+
+            <p v-if="!dataSource[d.dataIndex] && d.empty_description" v-html="d.empty_description"/>
+        </a-form-item>
+        <a-form-item v-if="$slots.supplement||$slots.action">
+            <slot name="supplement"/>
+            <slot name="action"/>
+        </a-form-item>
+    </a-form>
+</template>
+
+<script>
+import StdSelectOption from './StdSelectOption'
+import StdSelector from './StdSelector'
+import StdUpload from './StdUpload'
+import StdDatePicker from './StdDatePicker'
+import StdTransfer from './StdTransfer'
+import StdCheckTag from '@/components/StdDataEntry/StdCheckTag'
+import StdMultiCheckTag from '@/components/StdDataEntry/StdMultiCheckTag'
+import StdCheckGroup from '@/components/StdDataEntry/StdCheckGroup'
+import StdRadioGroup from '@/components/StdDataEntry/StdRadioGroup'
+
+export default {
+    name: 'StdDataEntry',
+    components: {
+        StdRadioGroup,
+        StdCheckGroup,
+        StdMultiCheckTag,
+        StdCheckTag,
+        StdTransfer,
+        StdDatePicker,
+        StdSelectOption,
+        StdSelector,
+        StdUpload
+    },
+    props: {
+        dataList: [Array, Object],
+        dataSource: Object,
+        error: {
+            type: Object,
+            default() {
+                return {}
+            }
+        },
+        layout: {
+            default: 'vertical',
+            validator: value => {
+                return ['horizontal', 'vertical', 'inline'].indexOf(value) !== -1
+            }
+        }
+    },
+    model: {
+        prop: 'dataSource',
+        event: 'changeDataSource'
+    },
+    data() {
+        return {
+            temp: null,
+            i: 0,
+            M_dataList: {}
+        }
+    },
+    watch: {
+        dataSource() {
+            this.temp = this.dataSource ?? []
+        },
+        dataList() {
+            this.M_dataList = this.editableColumns(this.dataList ?? [])
+        }
+    },
+    created() {
+        this.temp = this.dataSource ?? []
+        if (this.layout === 'horizontal') {
+            this.labelCol = {span: 4}
+            this.wrapperCol = {span: 18}
+        }
+        this.M_dataList = this.editableColumns(this.dataList)
+    },
+    methods: {
+        get_params_fn(d) {
+            return {...d.edit.get_params, ...this.bindModel(d.edit.bind, this.temp)}
+        },
+        fn: (obj, desc) => {
+            const arr = desc.split('.')
+            while (arr.length) {
+                const top = obj[arr.shift()]
+                if (top === undefined) {
+                    return null
+                }
+                obj = top
+            }
+            return obj
+        },
+        editableColumns(columns) {
+            if (typeof columns === 'object') {
+                columns = Object.values(columns)
+            }
+            return columns.filter((c) => {
+                return c.edit
+            })
+        },
+        bindModel(bind, dataSource) {
+            let object = {}
+            if (bind) {
+                for (const [key, value] of Object.entries(bind)) {
+                    object[key] = this.fn(dataSource, value)
+                }
+            }
+            return object
+        },
+        getInputPlaceholder(d, dataSource) {
+            // edit 模式
+            if (dataSource.id) {
+                return d.edit.placeholder?.edit ?? d.edit.placeholder
+            } else {
+                // add 模式
+                return d.edit.placeholder?.add ?? d.edit.placeholder
+            }
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 52 - 0
frontend-next/src/components/StdDataEntry/StdDatePicker.vue

@@ -0,0 +1,52 @@
+<template>
+    <a-date-picker
+        v-model="dateModel"
+        :show-time="showTime"
+        @change="r => {changeDate(r.format())}"
+    />
+</template>
+
+<script>
+import moment from 'moment'
+import 'moment/locale/zh-cn'
+
+moment.locale('zh-cn')
+
+export default {
+    name: 'StdDatePicker',
+    props: {
+        date: String,
+        showTime: {
+            type: [Object, Boolean],
+            default() {
+                return false
+            }
+        }
+
+    },
+    model: {
+        prop: 'date',
+        event: 'changeDate'
+    },
+    data() {
+        return {
+            moment,
+            dateModel: this.date ? moment(this.date) : null
+        }
+    },
+    watch: {
+        date() {
+            this.dateModel = this.date ? moment(this.date) : null
+        }
+    },
+    methods: {
+        changeDate(d) {
+            this.$emit('changeDate', d)
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 75 - 0
frontend-next/src/components/StdDataEntry/StdMultiCheckTag.vue

@@ -0,0 +1,75 @@
+<template>
+    <div>
+        <template v-for="(v,k) in options">
+            <a-checkable-tag
+                :key="k"
+                :checked="selectedTags.indexOf(k) > -1"
+                @change="checked => handleChange(k, checked)"
+            >
+                {{ v }}
+            </a-checkable-tag>
+        </template>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'StdMultiCheckTag',
+    data() {
+        return {
+            selectedTags: [],
+        }
+    },
+    props: {
+        disabled: [Boolean],
+        value: [Array],
+        dataObject: [Object],
+        options: {
+            type: Object,
+            default() {
+                return {}
+            }
+        },
+    },
+    model: {
+        prop: 'value',
+        event: 'change'
+    },
+    methods: {
+        handleChange(tag, checked) {
+            const {selectedTags} = this
+            this.selectedTags = checked
+                ? [...selectedTags, tag]
+                : selectedTags.filter(t => t !== tag)
+            this.$emit('change', this.selectedTags)
+        },
+        loadData() {
+            for (const [k] of Object.entries(this.options)) {
+                if (this.dataObject[k] === 1) {
+                    if (this.selectedTags.indexOf(k) === -1)
+                        this.selectedTags.push(k)
+                }
+            }
+        }
+    },
+    watch: {
+        value() {
+            this.selectedTag = this.value ?? []
+        },
+    },
+    created() {
+        this.selectedTag = this.value ?? []
+        this.loadData()
+    },
+}
+</script>
+
+<style lang="less" scoped>
+.ant-tag {
+    background-color: rgba(0, 0, 0, 0.05);
+}
+
+.ant-tag-checkable-checked {
+    background-color: #1890ff;
+}
+</style>

+ 153 - 0
frontend-next/src/components/StdDataEntry/StdMultiFilesUpload.vue

@@ -0,0 +1,153 @@
+<template>
+    <div>
+        <a-upload
+            :before-upload="beforeUpload"
+            :multiple="true"
+            :show-upload-list="true"
+            :file-list="uploadList"
+            :remove="remove"
+        >
+            <a-button :disabled="disabled">
+                <a-icon type="upload"/>
+                选择文件
+            </a-button>
+        </a-upload>
+        <a-progress
+            v-if="show_progress"
+            :stroke-color="{
+        from: '#108ee9',
+        to: '#87d068',
+      }"
+            :percent="progress"
+        />
+        <a-button
+            type="primary"
+            :disabled="uploadList.length === 0 && !id"
+            :loading="uploading"
+            style="margin: 16px 0"
+            @click="upload"
+            v-if="id"
+        >
+            {{ uploading ? '上传中' : '开始上传' }}
+        </a-button>
+        <p style="margin: 15px 0" v-for="file in uploaded" :key="file.id">
+            <a-icon type="paper-clip" style="margin-right: 5px"/>
+            <a :href="server + '/' + file.path" target="_blank" @click="()=>{}">{{ getFileName(file.path) }}</a>
+            <a-popconfirm
+                title="确定要删除文件吗"
+                ok-text="确认"
+                cancel-text="取消"
+                @confirm="deleteFile(file.id)"
+                style="float: right"
+            >
+                <a-button type="link">删除</a-button>
+            </a-popconfirm>
+        </p>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'StdMultiFilesUpload',
+    props: {
+        api: Function,
+        id: {
+            type: Number,
+            default: null
+        },
+        fileList: {
+            default: null
+        },
+        autoUpload: {
+            type: Boolean,
+            default: false
+        },
+        api_delete: {
+            type: Function,
+            default: null
+        },
+        disabled: {
+            type: Boolean,
+            default: false
+        }
+    },
+    watch: {
+        fileList() {
+            this.uploaded = this.fileList
+        }
+    },
+    data() {
+        return {
+            show_progress: false,
+            uploadList: [],
+            uploaded: this.fileList,
+            lastFileTime: 0,
+            server: process.env['VUE_APP_API_UPLOAD_ROOT'],
+            uploading: false,
+            progress: 0
+        }
+    },
+    model: {
+        prop: 'fileUrl',
+        event: 'changeFileUrl'
+    },
+    methods: {
+        async upload() {
+            if (this.uploadList.length) {
+                this.uploading = true
+                this.show_progress = true
+                this.progress = 0
+                let formData = new FormData()
+                this.uploadList.forEach(v => {
+                    formData.append('file[]', v)
+                })
+                this.visible = false
+                this.uploading = true
+                this.$message.info('正在上传附件, 请不要关闭本页')
+                let config = {
+                    onUploadProgress: (progressEvent) => {
+                        // 使用本地 progress 事件
+                        if (progressEvent.lengthComputable) {
+                            this.progress = Math.round((progressEvent.loaded / progressEvent.total) * 100) // 使用某种 UI 进度条组件会用到的百分比
+                        }
+                    }
+                }
+                return this.api(this.id, formData, config).then(r => {
+                    this.uploadList = []
+                    this.uploaded = [...this.uploaded, ...r]
+                    this.uploading = false
+                    this.$emit('uploaded', r)
+                    this.uploading = false
+                    this.orig = false
+                    this.$message.success('上传成功')
+                }).catch(e => {
+                    this.$message.error(e.message ? e.message : '上传失败')
+                })
+            }
+        },
+        beforeUpload(file) {
+            this.uploadList.push(file)
+            return false
+        },
+        deleteFile(file_id) {
+            this.api_delete(this.id, file_id).then(r => {
+                this.uploaded = r
+            })
+        },
+        getFileName(path) {
+            // 从15开始找
+            const idx = path.indexOf('/', 15)
+            return path.substring(idx + 1)
+        },
+        remove(r) {
+            this.uploadList = this.uploadList.filter(value => {
+                return value !== r
+            })
+        },
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 49 - 0
frontend-next/src/components/StdDataEntry/StdRadioGroup.vue

@@ -0,0 +1,49 @@
+<template>
+    <a-radio-group name="radioGroup" v-model="data" @change="onChange">
+        <a-radio :value="k" v-for="(v,k) in options" :key="k">
+            {{ v }}
+        </a-radio>
+    </a-radio-group>
+</template>
+
+
+<script>
+export default {
+    name: 'StdRadioGroup',
+    props: {
+        options: [Object, Array],
+        value: {
+            type: [String, Number]
+        },
+        keyType: String
+    },
+    model: {
+        prop: 'value',
+        event: 'changeValue'
+    },
+    data() {
+        return {
+            data: this.value?.toString() ?? '',
+        }
+    },
+    watch: {
+        value() {
+            this.data = this.value.toString()
+        }
+    },
+    methods: {
+        onChange(e) {
+            if (this.keyType === 'int') {
+                this.data = e.target.value
+                this.$emit('changeValue', parseInt(e.target.value))
+            } else {
+                this.$emit('changeValue', e.target.value)
+            }
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 47 - 0
frontend-next/src/components/StdDataEntry/StdSelectOption.vue

@@ -0,0 +1,47 @@
+<template>
+    <a-select
+        v-model="tempValue"
+        :defaultValue="Object.keys(options)[0]"
+        @change="$emit('change', isNaN(parseInt(tempValue)) || keyType === 'string' ? tempValue : parseInt(tempValue) )">
+        <a-select-option v-for="(v,k) in options" :key="k">{{ v }}</a-select-option>
+    </a-select>
+</template>
+
+<script>
+export default {
+    name: 'StdSelectOption',
+    props: {
+        value: [Number, String, Boolean],
+        options: [Array, Object],
+        keyType: {
+            type: String,
+            default() {
+                return 'int'
+            }
+        }
+    },
+    model: {
+        prop: 'value',
+        event: 'change'
+    },
+    data() {
+        return {
+            tempValue: null
+        }
+    },
+    watch: {
+        value() {
+            this.tempValue = this.value != null ? this.value.toString() : null
+        }
+    },
+    created() {
+        this.tempValue = this.value != null ? this.value.toString() : null
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-select {
+    min-width: 80px;
+}
+</style>

+ 156 - 0
frontend-next/src/components/StdDataEntry/StdSelector.vue

@@ -0,0 +1,156 @@
+<template>
+    <div class="std-selector" @click="show()">
+        <a-input v-model="_key" disabled hidden/>
+        <div class="value">
+            <p>{{ M_value }}</p>
+        </div>
+        <a-modal
+            :mask="false"
+            :visible="visible"
+            cancel-text="取消"
+            ok-text="选择"
+            title="选择器"
+            @cancel="visible=false"
+            @ok="ok()"
+            :width="600"
+            destroyOnClose
+        >
+            {{ description }}
+            <std-table
+                :api="api"
+                :columns="columns"
+                :data_key="data_key"
+                :disable_search="disable_search"
+                :pithy="true"
+                :get_params="get_params"
+                :selectionType="selectionType"
+                :disable_query_params="true"
+                @selected="onSelect"
+                @selectedRecord="onSelectedRecord"
+            />
+        </a-modal>
+    </div>
+</template>
+
+<script>
+
+export default {
+    name: 'StdSelector',
+    components: {
+        StdTable: () => import('@/components/StdDataDisplay/StdTable')
+    },
+    props: {
+        _key: [Number, String],
+        value: String,
+        recordValueIndex: [Number, String],
+        selectionType: {
+            type: String,
+            default: 'checkbox',
+            validator: function (value) {
+                return ['checkbox', 'radio'].indexOf(value) !== -1
+            }
+        },
+        api: Object,
+        columns: Array,
+        data_key: String,
+        disable_search: {
+            type: Boolean,
+            default: false
+        },
+        get_params: {
+            type: Object,
+            default() {
+                return {}
+            }
+        },
+        description: String
+    },
+    model: {
+        prop: '_key',
+        event: 'changeSelect'
+    },
+    data() {
+        return {
+            visible: false,
+            selected: [],
+            record: {},
+            M_value: this.value
+        }
+    },
+    watch: {
+        _key() {
+            if (!this._key) {
+                this.M_value = null
+            }
+        },
+        value() {
+            this.M_value = this.value
+        }
+    },
+    methods: {
+        show() {
+            this.visible = true
+        },
+        onSelect(selected) {
+            this.selected = selected
+        },
+        onSelectedRecord(r) {
+            this.record = r
+        },
+        ok() {
+            this.visible = false
+            let selected = this.selected
+            if (this.selectionType === 'radio') {
+                selected = this.selected[0]
+            }
+            this.M_value = this.record[this.recordValueIndex]
+            this.$emit('changeSelect', selected)
+        }
+    }
+}
+</script>
+
+<style scoped>
+.ant-form-inline .std-selector {
+    height: 40px;
+}
+</style>
+
+<style lang="less" scoped>
+.std-selector {
+    height: 38px;
+    min-width: 180px;
+    position: relative;
+
+    .value {
+        box-sizing: border-box;
+        font-variant: tabular-nums;
+        list-style: none;
+        font-feature-settings: 'tnum';
+        position: absolute;
+        top: 50%;
+        bottom: 50%;
+        left: 50%;
+        -webkit-transform: translateX(-50%) translateY(-50%);
+        display: inline-block;
+        width: 100%;
+        height: 32px;
+        padding: 4px 11px;
+        color: rgba(0, 0, 0, 0.65);
+        font-size: 14px;
+        line-height: 1.5;
+        background-color: #fff;
+        background-image: none;
+        border: 1px solid #d9d9d9;
+        border-radius: 4px;
+        transition: all 0.3s;
+        margin: 0 10px 0 0;
+        cursor: pointer;
+        @media (prefers-color-scheme: dark) {
+            background-color: #1e1f20;
+            border: 1px solid #666666;
+            color: rgba(255, 255, 255, 0.99);
+        }
+    }
+}
+</style>

+ 105 - 0
frontend-next/src/components/StdDataEntry/StdSingleFileUpload.vue

@@ -0,0 +1,105 @@
+<template>
+    <div>
+        <a-upload
+            :before-upload="beforeUpload"
+            :multiple="false"
+            :file-list="uploadList"
+            :remove="remove"
+        >
+            <a-button :disabled="disabled">
+                <a-icon type="upload"/>
+                上传
+            </a-button>
+        </a-upload>
+        <a-progress
+            v-if="show_progress"
+            :stroke-color="{from: '#108ee9',to: '#87d068'}"
+            :percent="progress"
+        />
+        <p style="margin: 15px 0" v-show="fileUrl">
+            <a-icon type="paper-clip" style="margin-right: 5px"/>
+            <a :href="server + '/' + fileUrl" target="_blank" @click="()=>{}">下载附件</a>
+        </p>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'StdSingleFileUpload',
+    props: {
+        api: Function,
+        id: {
+            type: Number,
+            default: null
+        },
+        fileUrl: {
+            default: null
+        },
+        autoUpload: {
+            type: Boolean,
+            default: false
+        },
+        disabled: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data() {
+        return {
+            uploadList: [],
+            server: process.env['VUE_APP_API_UPLOAD_ROOT'],
+            progress: 0,
+            show_progress: false,
+        }
+    },
+    model: {
+        prop: 'fileUrl',
+        event: 'changeFileUrl'
+    },
+    methods: {
+        remove() {
+            this.uploadList.shift()
+        },
+        async upload() {
+            if (this.uploadList.length) {
+                this.show_progress = true
+                this.progress = 0
+                const formData = new FormData()
+                formData.append('file', this.uploadList.shift())
+                this.visible = false
+                this.uploading = true
+                this.$message.info('正在上传附件, 请不要关闭本页')
+                let config = {
+                    onUploadProgress: (progressEvent) => {
+                        // 使用本地 progress 事件
+                        if (progressEvent.lengthComputable) {
+                            this.progress = Math.round((progressEvent.loaded / progressEvent.total) * 100)
+                        }
+                    }
+                }
+                return this.api(this.id, formData, config).then(r => {
+                    this.$emit('uploaded', r.url)
+                    this.$emit('changeFileUrl', r.url)
+                    this.uploading = false
+                    this.$message.success('上传成功')
+                }).catch(e => {
+                    this.$message.error(e.message ? e.message : '上传失败')
+                })
+            }
+        },
+        beforeUpload(file) {
+            this.uploadList = [file]
+            this.$emit('changeFileUrl', file.name)
+            // 有自动上传参数就自动上传,没有就看 id, 没有 id 就不上传
+            if (this.autoUpload ? this.autoUpload : (!!this.id)) {
+                this.upload()
+            }
+            return false
+        },
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 75 - 0
frontend-next/src/components/StdDataEntry/StdTransfer.vue

@@ -0,0 +1,75 @@
+<template>
+    <a-transfer
+        :data-source="dataSource"
+        :render="item=>item.title"
+        :selectedKeys="selectedKeys"
+        :targetKeys="targetKeys"
+        :titles="['可添加', '已添加']"
+        @change="handleChange"
+        @selectChange="handleSelectChange"
+    />
+</template>
+
+<script>
+const mockData = []
+for (let i = 0; i < 20; i++) {
+    mockData.push({
+        key: i.toString(),
+        title: `content${i + 1}`,
+        description: `description of content${i + 1}`
+    })
+}
+export default {
+    name: 'StdTransfer',
+    props: {
+        api: Function,
+        dataKey: String,
+        target: String
+    },
+    model: {
+        prop: 'target',
+        event: 'changeTarget'
+    },
+    data() {
+        return {
+            targetKeys: [],
+            selectedKeys: [],
+            dataSource: []
+        }
+    },
+    created() {
+        this.targetKeys = this.target.split(',')
+        if (this.api) {
+            this.api().then(r => {
+                const dataSource = []
+                r[this.dataKey ? this.dataKey : 'data'].forEach(v => {
+                    dataSource.push({
+                        key: v.id.toString(),
+                        title: `${v.title}`,
+                        description: `${v.description}`
+                    })
+                })
+                this.dataSource = dataSource
+            })
+        }
+    },
+    watch: {
+        targetKeys() {
+            this.$emit('changeTarget', this.targetKeys.toString())
+        }
+    },
+    methods: {
+        handleChange(nextTargetKeys) {
+            this.targetKeys = nextTargetKeys
+        },
+        handleSelectChange(sourceSelectedKeys, targetSelectedKeys) {
+            this.selectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
+
+        },
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 266 - 0
frontend-next/src/components/StdDataEntry/StdUpload.vue

@@ -0,0 +1,266 @@
+<template>
+    <div v-if="type==='img'">
+        <a-upload
+            :before-upload="beforeUpload"
+            :show-upload-list="false"
+            class="avatar-uploader"
+            list-type="picture-card"
+        >
+            <img v-if="fileUrl" :src="getFileUrl()" width="100">
+            <div v-else>
+                <a-icon :type="uploading ? 'loading' : 'plus'"/>
+                <div class="ant-upload-text">
+                    上传图片
+                </div>
+            </div>
+        </a-upload>
+
+        <a-modal
+            v-if="crop"
+            v-model="visible"
+            cancelText="取消上传"
+            class="cropper"
+            okText="裁切"
+            title="图片裁切"
+            @cancel="visible=false;$emit('changeFileUrl', orig)"
+            @ok="handleCropSuccess"
+        >
+            <div class="vue-cropper" v-if="fileUrl.substring(0,5) === 'data:'">
+                <VueCropper
+                    ref="cropper"
+                    :autoCrop="true"
+                    :autoCropHeight="cropOptions.autoCropHeight"
+                    :autoCropWidth="cropOptions.autoCropWidth"
+                    :fixed="cropOptions.fixed"
+                    :fixedNumber="cropOptions.fixedNumber"
+                    :img="getFileUrl()"
+                    outputType="png"
+                />
+            </div>
+            <div style="margin: 10px 0">
+                <a-button @click="handleSingleUpload">不剪裁</a-button>
+            </div>
+        </a-modal>
+    </div>
+
+    <div v-else-if="type==='file'">
+        <std-single-file-upload
+            :file-url="fileUrl"
+            :id="id"
+            :api="api"
+            :auto-upload="autoUpload"
+            @changeFileUrl="url => {$emit('changeFileUrl', url)}"
+            @uploaded="url => {$emit('uploaded', url)}"
+            :disabled="disabled"
+            ref="single-file"
+        />
+    </div>
+
+    <div v-else-if="type==='multi-file'">
+        <std-multi-files-upload
+            :file-list="M_list"
+            :id="id"
+            :api="api"
+            :auto-upload="autoUpload"
+            :api_delete="api_delete"
+            @changeFileUrl="url => {$emit('changeFileUrl', url)}"
+            @uploaded="url => {$emit('uploaded', url)}"
+            :disabled="disabled"
+            ref="multi-file"
+        />
+    </div>
+
+</template>
+
+<script>
+import Vue from 'vue'
+import VueCropper from 'vue-cropper'
+import StdSingleFileUpload from '@/components/StdDataEntry/StdSingleFileUpload'
+import StdMultiFilesUpload from '@/components/StdDataEntry/StdMultiFilesUpload'
+import {v4 as uuidv4} from 'uuid'
+
+Vue.use(VueCropper)
+
+export default {
+    name: 'StdUpload',
+    components: {StdMultiFilesUpload, StdSingleFileUpload},
+    props: {
+        id: {
+            type: Number,
+            default: null
+        },
+        api: Function,
+        api_delete: {
+            type: Function,
+            default: null
+        },
+        fileUrl: {
+            default: ''
+        },
+        autoUpload: {
+            type: Boolean,
+            default: false
+        },
+        type: {
+            default: 'img',
+            validator: value => {
+                return ['img', 'file', 'multi-file'].indexOf(value) !== -1
+            }
+        },
+        crop: {
+            type: Boolean,
+            default: false
+        },
+        cropOptions: {
+            type: Object,
+            default: () => {
+                return {
+                    fixed: true,
+                    autoCropWidth: 200,
+                    autoCropHeight: 200,
+                }
+            }
+        },
+        list: {
+            default: null
+        },
+        disabled: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data() {
+        return {
+            uploading: false,
+            orig: '',
+            visible: false,
+            fileList: [],
+            M_list: this.list,
+            server: process.env['VUE_APP_API_UPLOAD_ROOT']
+        }
+    },
+    created() {
+        this.orig = this.fileUrl
+    },
+    model: {
+        prop: 'fileUrl',
+        event: 'changeFileUrl'
+    },
+    watch: {
+        list() {
+            this.M_list = this.list
+        }
+    },
+    methods: {
+        getFileUrl() {
+            return this.fileUrl.substring(0, 5) === 'data:' ? this.fileUrl :
+                this.server + '/' + this.fileUrl
+        },
+        async upload() {
+            if (this.type === 'multi-file') {
+                return await this.$refs['multi-file'].upload()
+            }
+            if (this.orig && this.fileUrl !== this.orig) {
+                return this.handleSingleUpload()
+            }
+            if (this.$refs['single-file']) {
+                return await this.$refs['single-file'].upload()
+            }
+        },
+        handleSingleUpload() {
+            const formData = new FormData()
+            formData.append('file', this.fileList[0])
+            this.visible = false
+            this.uploading = true
+            this.$message.info('正在上传附件, 请不要关闭本页')
+
+            return this.api(this.id, formData).then(r => {
+                this.$emit('uploaded', r.url)
+                this.$emit('changeFileUrl', r.url)
+                this.uploading = false
+                this.$message.success('上传成功')
+                this.orig = r.url
+            })
+
+        },
+        beforeUpload(file) {
+            // 赋予新值之前做个备份 emm 生气了哼!!!
+            this.orig = this.fileUrl ? this.fileUrl : 'orig_is_empty'
+            this.fileList = [file]
+            if (this.type === 'img') {
+                this.visible = true
+                const r = new FileReader()
+                r.readAsDataURL(file)
+                r.onload = e => {
+                    file.thumbUrl = e.target.result
+                    this.$emit('changeFileUrl', e.target.result)
+                }
+                if (this.autoUpload) {
+                    this.handleSingleUpload()
+                    return false
+                }
+            } else {
+                this.$emit('changeFileUrl', file.name)
+            }
+            return false
+        },
+        afterCropUpload(file) {
+            this.visible = true
+            const r = new FileReader()
+            r.readAsDataURL(file)
+            r.onload = e => {
+                file.thumbUrl = e.target.result
+                this.$emit('changeFileUrl', e.target.result)
+            }
+            this.fileList = [file]
+            this.$nextTick(() => {
+                this.handleSingleUpload()
+            })
+        },
+        handleCropSuccess() {
+            this.$refs.cropper.getCropBlob((data) => {
+                let file = new window.File([data], uuidv4() + '.png', {type: data.type})
+                this.afterCropUpload(file)
+                this.visible = false
+            })
+        },
+        remove(r) {
+            this.fileList = this.fileList.filter(value => {
+                return value !== r
+            })
+        },
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.upload-picture-btn {
+    font-size: 20px;
+    color: #999999;
+}
+
+.cropper {
+    .ant-modal-body {
+        min-height: 256px;
+    }
+}
+
+.vue-cropper {
+    min-height: 200px;
+    background-image: unset;
+}
+
+.img-preview {
+    float: left;
+    border: 1px solid #8e8e904d;
+    border-radius: 5px;
+    margin: 5px;
+    padding: 5px;
+
+    img {
+        height: 90px;
+        width: 90px;
+        object-fit: cover;
+    }
+}
+</style>

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

@@ -0,0 +1 @@
+@import "ant-design-vue/lib/style/themes/dark.less";

+ 12 - 0
frontend-next/src/gettext.ts

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

+ 1 - 0
frontend-next/src/language/LINGUAS

@@ -0,0 +1 @@
+en zh_CN zh_TW

+ 508 - 0
frontend-next/src/language/en/app.po

@@ -0,0 +1,508 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Project-Id-Version: PACKAGE VERSION\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/routes/index.ts:105
+msgid "404 Not Found"
+msgstr "404 Not Found"
+
+#: src/routes/index.ts:83
+msgid "About"
+msgstr "About"
+
+#: src/views/config/Config.vue:37 src/views/domain/DomainList.vue:56
+#: src/views/user/User.vue:41
+msgid "Action"
+msgstr "Action"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:19
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:21
+msgid "Add Directive Below"
+msgstr "Add Directive Below"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:17
+#: src/views/domain/ngx_conf/LocationEditor.vue:30
+msgid "Add Location"
+msgstr "Add Location"
+
+#: src/routes/index.ts:45 src/views/domain/DomainAdd.vue:2
+msgid "Add Site"
+msgstr "Add Site"
+
+#: src/views/domain/DomainEdit.vue:16
+msgid "Advance Mode"
+msgstr "Advance Mode"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:7
+msgid "Are you sure you want to remove this directive?"
+msgstr "Are you sure you want to remove this directive?"
+
+#: src/views/domain/cert/IssueCert.vue:123
+msgid "Auto-renewal disabled for %{name}"
+msgstr "Auto-renewal disabled for %{name}"
+
+#: src/views/domain/cert/IssueCert.vue:117
+msgid "Auto-renewal enabled for %{name}"
+msgstr "Auto-renewal enabled for %{name}"
+
+#: src/views/domain/DomainEdit.vue:47
+msgid "Back"
+msgstr "Back"
+
+#: src/views/domain/DomainAdd.vue:5
+msgid "Base information"
+msgstr "Base information"
+
+#: src/views/domain/DomainEdit.vue:19
+msgid "Basic Mode"
+msgstr "Basic Mode"
+
+#: src/views/other/About.vue:12
+msgid "Build with"
+msgstr "Build with"
+
+#: src/views/config/ConfigEdit.vue:7
+msgid "Cancel"
+msgstr "Cancel"
+
+#: src/views/domain/cert/CertInfo.vue:13
+msgid "Certificate has expired"
+msgstr "Certificate has expired"
+
+#: src/views/domain/cert/CertInfo.vue:17
+msgid "Certificate is valid"
+msgstr "Certificate is valid"
+
+#: src/views/domain/cert/CertInfo.vue:3
+msgid "Certificate Status"
+msgstr "Certificate Status"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:18
+#: src/views/domain/ngx_conf/LocationEditor.vue:18
+#: src/views/domain/ngx_conf/LocationEditor.vue:6
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:23
+msgid "Comments"
+msgstr "Comments"
+
+#: src/views/domain/DomainAdd.vue:11
+msgid "Configuration Name"
+msgstr "Configuration Name"
+
+#: src/views/config/Config.vue:2
+msgid "Configurations"
+msgstr "Configurations"
+
+#: src/views/domain/DomainAdd.vue:6
+msgid "Configure SSL"
+msgstr "Configure SSL"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:12
+#: src/views/domain/ngx_conf/LocationEditor.vue:24
+msgid "Content"
+msgstr "Content"
+
+#: src/views/dashboard/DashBoard.vue:64
+msgid "CPU Status"
+msgstr "CPU Status"
+
+#: src/views/dashboard/DashBoard.vue:23
+msgid "CPU:"
+msgstr "CPU:"
+
+#: src/views/domain/DomainAdd.vue:65
+msgid "Create Another"
+msgstr "Create Another"
+
+#: src/views/user/User.vue:29
+msgid "Created at"
+msgstr "Created at"
+
+#: src/routes/index.ts:17
+msgid "Dashboard"
+msgstr "Dashboard"
+
+#: src/views/other/Install.vue:63
+msgid "Database (Optional, default: database)"
+msgstr "Database (Optional, default: database)"
+
+#: src/views/other/About.vue:34
+msgid "Development Mode"
+msgstr "Development Mode"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:14
+msgid "Directive"
+msgstr "Directive"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
+msgid "Directives"
+msgstr "Directives"
+
+#: src/views/domain/cert/IssueCert.vue:125
+msgid "Disable auto-renewal failed for %{name}"
+msgstr "Disable auto-renewal failed for %{name}"
+
+#: src/views/domain/DomainEdit.vue:10 src/views/domain/DomainList.vue:17
+#: src/views/domain/DomainList.vue:44
+msgid "Disabled"
+msgstr "Disabled"
+
+#: src/views/domain/DomainEdit.vue:164 src/views/domain/DomainList.vue:82
+msgid "Disabled successfully"
+msgstr "Disabled successfully"
+
+#: src/views/dashboard/DashBoard.vue:96
+msgid "Disk IO"
+msgstr "Disk IO"
+
+#: src/views/domain/DomainAdd.vue:58
+msgid "Domain Config Created Successfully"
+msgstr "Domain Config Created Successfully"
+
+#: src/views/domain/DomainEdit.vue:5
+msgid "Edit %{n}"
+msgstr "Edit %{n}"
+
+#: src/routes/index.ts:67 src/views/config/ConfigEdit.vue:2
+msgid "Edit Configuration"
+msgstr "Edit Configuration"
+
+#: src/routes/index.ts:49
+msgid "Edit Site"
+msgstr "Edit Site"
+
+#: src/views/other/Install.vue:25
+msgid "Email (*)"
+msgstr "Email (*)"
+
+#: src/views/domain/cert/IssueCert.vue:119
+msgid "Enable auto-renewal failed for %{name}"
+msgstr "Enable auto-renewal failed for %{name}"
+
+#: src/views/domain/DomainAdd.vue:113
+msgid "Enable failed"
+msgstr "Enable failed"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:3
+msgid "Enable TLS"
+msgstr "Enable TLS"
+
+#: src/views/domain/DomainEdit.vue:29 src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainList.vue:12 src/views/domain/DomainList.vue:20
+#: src/views/domain/DomainList.vue:43
+msgid "Enabled"
+msgstr "Enabled"
+
+#: src/views/domain/DomainAdd.vue:110 src/views/domain/DomainEdit.vue:156
+#: src/views/domain/DomainList.vue:73
+msgid "Enabled successfully"
+msgstr "Enabled successfully"
+
+#: src/views/domain/cert/IssueCert.vue:3
+msgid "Encrypt website with Let's Encrypt"
+msgstr "Encrypt website with Let's Encrypt"
+
+#: src/views/domain/cert/CertInfo.vue:6
+msgid "Expiration Date: %{date}"
+msgstr "Expiration Date: %{date}"
+
+#: src/views/domain/DomainEdit.vue:167 src/views/domain/DomainList.vue:86
+msgid "Failed to disable %{msg}"
+msgstr "Failed to disable %{msg}"
+
+#: src/views/domain/DomainEdit.vue:159 src/views/domain/DomainList.vue:77
+msgid "Failed to enable %{msg}"
+msgstr "Failed to enable %{msg}"
+
+#: src/views/other/Error.vue:3 src/views/other/Error.vue:4
+msgid "File Not Found"
+msgstr "File Not Found"
+
+#: src/views/domain/DomainAdd.vue:7
+msgid "Finished"
+msgstr "Finished"
+
+#: src/views/domain/methods.js:6
+msgid "Getting the certificate, please wait..."
+msgstr "Getting the certificate, please wait..."
+
+#: src/routes/index.ts:10
+msgid "Home"
+msgstr "Home"
+
+#: src/routes/index.ts:93 src/views/other/Install.vue:70
+msgid "Install"
+msgstr "Install"
+
+#: src/views/domain/cert/CertInfo.vue:4
+msgid "Intermediate Certification Authorities: %{issuer}"
+msgstr "Intermediate Certification Authorities: %{issuer}"
+
+#: src/views/other/Install.vue:18
+msgid "Invalid E-mail!"
+msgstr "Invalid E-mail!"
+
+#: src/views/user/User.vue:25
+msgid "Leave blank for no change"
+msgstr "Leave blank for no change"
+
+#: src/views/dashboard/DashBoard.vue:11
+msgid "Load Averages:"
+msgstr "Load Averages:"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:5
+msgid "Location"
+msgstr "Location"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:2
+msgid "Locations"
+msgstr "Locations"
+
+#: src/routes/index.ts:99 src/views/other/Login.vue:37
+msgid "Login"
+msgstr "Login"
+
+#: src/views/other/Login.vue:79
+msgid "Login successful"
+msgstr "Login successful"
+
+#: src/views/layouts/HeaderLayout.vue:29
+msgid "Logout successful"
+msgstr "Logout successful"
+
+#: src/views/domain/cert/IssueCert.vue:34
+msgid "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate."
+msgstr "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate."
+
+#: src/routes/index.ts:58
+msgid "Manage Configs"
+msgstr "Manage Configs"
+
+#: src/routes/index.ts:33 src/views/domain/DomainList.vue:2
+msgid "Manage Sites"
+msgstr "Manage Sites"
+
+#: src/routes/index.ts:25
+msgid "Manage Users"
+msgstr "Manage Users"
+
+#: src/views/dashboard/DashBoard.vue:32
+msgid "Memory"
+msgstr "Memory"
+
+#: src/views/dashboard/DashBoard.vue:29
+msgid "Memory and Storage"
+msgstr "Memory and Storage"
+
+#: src/views/domain/DomainAdd.vue:62
+msgid "Modify Config"
+msgstr "Modify Config"
+
+#: src/views/config/Config.vue:24 src/views/domain/DomainList.vue:32
+msgid "Name"
+msgstr "Name"
+
+#: src/views/dashboard/DashBoard.vue:74
+msgid "Network"
+msgstr "Network"
+
+#: src/views/dashboard/DashBoard.vue:48
+msgid "Network Statistics"
+msgstr "Network Statistics"
+
+#: src/views/dashboard/DashBoard.vue:52
+msgid "Network Total Receive"
+msgstr "Network Total Receive"
+
+#: src/views/dashboard/DashBoard.vue:56
+msgid "Network Total Send"
+msgstr "Network Total Send"
+
+#: src/views/domain/DomainAdd.vue:51
+msgid "Next"
+msgstr "Next"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:9
+msgid "No"
+msgstr "No"
+
+#: src/routes/index.ts:111
+msgid "Not Found"
+msgstr "Not Found"
+
+#: src/views/domain/cert/CertInfo.vue:8
+msgid "Not Valid Before: %{date}"
+msgstr "Not Valid Before: %{date}"
+
+#: src/views/domain/cert/IssueCert.vue:26
+msgid "Note: The server_name in the current configuration must be the domain name you need to get the certificate."
+msgstr "Note: The server_name in the current configuration must be the domain name you need to get the certificate."
+
+#: src/views/dashboard/DashBoard.vue:17
+msgid "OS:"
+msgstr "OS:"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:15
+msgid "Params"
+msgstr "Params"
+
+#: src/views/other/Login.vue:30 src/views/user/User.vue:19
+msgid "Password"
+msgstr "Password"
+
+#: src/views/other/Install.vue:48
+msgid "Password (*)"
+msgstr "Password (*)"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:21
+#: src/views/domain/ngx_conf/LocationEditor.vue:9
+msgid "Path"
+msgstr "Path"
+
+#: src/views/other/Install.vue:22
+msgid "Please input your E-mail!"
+msgstr "Please input your E-mail!"
+
+#: src/views/other/Install.vue:45 src/views/other/Login.vue:27
+msgid "Please input your password!"
+msgstr "Please input your password!"
+
+#: src/views/other/Install.vue:34 src/views/other/Login.vue:16
+msgid "Please input your username!"
+msgstr "Please input your username!"
+
+#: src/views/other/About.vue:9
+msgid "Project Team"
+msgstr "Project Team"
+
+#: src/views/dashboard/DashBoard.vue:107 src/views/dashboard/DashBoard.vue:183
+msgid "Reads"
+msgstr "Reads"
+
+#: src/views/dashboard/DashBoard.vue:173 src/views/dashboard/DashBoard.vue:78
+msgid "Receive"
+msgstr "Receive"
+
+#: src/views/config/ConfigEdit.vue:10 src/views/domain/DomainEdit.vue:50
+msgid "Save"
+msgstr "Save"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:21
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:22
+msgid "Save Directive"
+msgstr "Save Directive"
+
+#: src/views/config/ConfigEdit.vue:64 src/views/domain/DomainAdd.vue:117
+#: src/views/domain/DomainEdit.vue:148
+msgid "Save error %{msg}"
+msgstr "Save error %{msg}"
+
+#: src/views/config/ConfigEdit.vue:61 src/views/domain/DomainAdd.vue:107
+#: src/views/domain/DomainEdit.vue:143
+msgid "Saved successfully"
+msgstr "Saved successfully"
+
+#: src/views/dashboard/DashBoard.vue:176 src/views/dashboard/DashBoard.vue:85
+msgid "Send"
+msgstr "Send"
+
+#: src/views/config/ConfigEdit.vue:52 src/views/domain/DomainEdit.vue:110
+#: src/views/domain/DomainEdit.vue:121 src/views/domain/DomainEdit.vue:129
+#: src/views/other/Login.vue:84
+msgid "Server error"
+msgstr "Server error"
+
+#: src/views/dashboard/DashBoard.vue:5
+msgid "Server Info"
+msgstr "Server Info"
+
+#: src/views/domain/cert/IssueCert.vue:74
+msgid "server_name not found in directives"
+msgstr "server_name not found in directives"
+
+#: src/views/domain/cert/IssueCert.vue:17 src/views/domain/DomainAdd.vue:26
+msgid "server_name parameter is required"
+msgstr "server_name parameter is required"
+
+#: src/views/domain/cert/IssueCert.vue:20
+#: src/views/domain/cert/IssueCert.vue:80
+msgid "server_name parameters more than one"
+msgstr "server_name parameters more than one"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:6
+msgid "Single Directive"
+msgstr "Single Directive"
+
+#: src/routes/index.ts:41
+msgid "Sites List"
+msgstr "Sites List"
+
+#: src/views/domain/DomainList.vue:38
+msgid "Status"
+msgstr "Status"
+
+#: src/views/dashboard/DashBoard.vue:41
+msgid "Storage"
+msgstr "Storage"
+
+#: src/views/domain/cert/CertInfo.vue:5
+msgid "Subject Name: %{name}"
+msgstr "Subject Name: %{name}"
+
+#: src/views/dashboard/DashBoard.vue:36
+msgid "Swap"
+msgstr "Swap"
+
+#: src/routes/index.ts:75 src/views/pty/Terminal.vue:2
+msgid "Terminal"
+msgstr "Terminal"
+
+#: src/views/domain/cert/IssueCert.vue:30
+msgid "The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued."
+msgstr "The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued."
+
+#: src/views/other/Install.vue:59
+msgid "The filename cannot contain the following characters: %{c}"
+msgstr "The filename cannot contain the following characters: %{c}"
+
+#: src/views/config/Config.vue:30 src/views/domain/DomainList.vue:49
+#: src/views/user/User.vue:35
+msgid "Updated at"
+msgstr "Updated at"
+
+#: src/views/dashboard/DashBoard.vue:7
+msgid "Uptime:"
+msgstr "Uptime:"
+
+#: src/views/other/Login.vue:18 src/views/user/User.vue:11
+msgid "Username"
+msgstr "Username"
+
+#: src/views/other/Install.vue:36
+msgid "Username (*)"
+msgstr "Username (*)"
+
+#: src/views/domain/cert/IssueCert.vue:12 src/views/domain/DomainAdd.vue:21
+msgid "Warning"
+msgstr "Warning"
+
+#: src/views/dashboard/DashBoard.vue:100 src/views/dashboard/DashBoard.vue:180
+msgid "Writes"
+msgstr "Writes"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:8
+msgid "Yes"
+msgstr "Yes"
+
+#: src/views/other/About.vue:17
+msgctxt "Project"
+msgid "License"
+msgstr "License"

+ 533 - 0
frontend-next/src/language/messages.pot

@@ -0,0 +1,533 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+
+#: src/routes/index.ts:105
+msgid "404 Not Found"
+msgstr ""
+
+#: src/routes/index.ts:83
+msgid "About"
+msgstr ""
+
+#: src/views/config/Config.vue:37
+#: src/views/domain/DomainList.vue:56
+#: src/views/user/User.vue:41
+msgid "Action"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:19
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:21
+msgid "Add Directive Below"
+msgstr ""
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:17
+#: src/views/domain/ngx_conf/LocationEditor.vue:30
+msgid "Add Location"
+msgstr ""
+
+#: src/routes/index.ts:45
+#: src/views/domain/DomainAdd.vue:2
+msgid "Add Site"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:16
+msgid "Advance Mode"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:7
+msgid "Are you sure you want to remove this directive?"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:123
+msgid "Auto-renewal disabled for %{name}"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:117
+msgid "Auto-renewal enabled for %{name}"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:47
+msgid "Back"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:5
+msgid "Base information"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:19
+msgid "Basic Mode"
+msgstr ""
+
+#: src/views/other/About.vue:12
+msgid "Build with"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:7
+msgid "Cancel"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:13
+msgid "Certificate has expired"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:17
+msgid "Certificate is valid"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:3
+msgid "Certificate Status"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:18
+#: src/views/domain/ngx_conf/LocationEditor.vue:18
+#: src/views/domain/ngx_conf/LocationEditor.vue:6
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:23
+msgid "Comments"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:11
+msgid "Configuration Name"
+msgstr ""
+
+#: src/views/config/Config.vue:2
+msgid "Configurations"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:6
+msgid "Configure SSL"
+msgstr ""
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:12
+#: src/views/domain/ngx_conf/LocationEditor.vue:24
+msgid "Content"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:64
+msgid "CPU Status"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:23
+msgid "CPU:"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:65
+msgid "Create Another"
+msgstr ""
+
+#: src/views/user/User.vue:29
+msgid "Created at"
+msgstr ""
+
+#: src/routes/index.ts:17
+msgid "Dashboard"
+msgstr ""
+
+#: src/views/other/Install.vue:63
+msgid "Database (Optional, default: database)"
+msgstr ""
+
+#: src/views/other/About.vue:34
+msgid "Development Mode"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:14
+msgid "Directive"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
+msgid "Directives"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:125
+msgid "Disable auto-renewal failed for %{name}"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:10
+#: src/views/domain/DomainList.vue:17
+#: src/views/domain/DomainList.vue:44
+msgid "Disabled"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:164
+#: src/views/domain/DomainList.vue:82
+msgid "Disabled successfully"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:96
+msgid "Disk IO"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:58
+msgid "Domain Config Created Successfully"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:5
+msgid "Edit %{n}"
+msgstr ""
+
+#: src/routes/index.ts:67
+#: src/views/config/ConfigEdit.vue:2
+msgid "Edit Configuration"
+msgstr ""
+
+#: src/routes/index.ts:49
+msgid "Edit Site"
+msgstr ""
+
+#: src/views/other/Install.vue:25
+msgid "Email (*)"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:119
+msgid "Enable auto-renewal failed for %{name}"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:113
+msgid "Enable failed"
+msgstr ""
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:3
+msgid "Enable TLS"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:29
+#: src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainList.vue:12
+#: src/views/domain/DomainList.vue:20
+#: src/views/domain/DomainList.vue:43
+msgid "Enabled"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:110
+#: src/views/domain/DomainEdit.vue:156
+#: src/views/domain/DomainList.vue:73
+msgid "Enabled successfully"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:3
+msgid "Encrypt website with Let's Encrypt"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:6
+msgid "Expiration Date: %{date}"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:167
+#: src/views/domain/DomainList.vue:86
+msgid "Failed to disable %{msg}"
+msgstr ""
+
+#: src/views/domain/DomainEdit.vue:159
+#: src/views/domain/DomainList.vue:77
+msgid "Failed to enable %{msg}"
+msgstr ""
+
+#: src/views/other/Error.vue:3
+#: src/views/other/Error.vue:4
+msgid "File Not Found"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:7
+msgid "Finished"
+msgstr ""
+
+#: src/views/domain/methods.js:6
+msgid "Getting the certificate, please wait..."
+msgstr ""
+
+#: src/routes/index.ts:10
+msgid "Home"
+msgstr ""
+
+#: src/routes/index.ts:93
+#: src/views/other/Install.vue:70
+msgid "Install"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:4
+msgid "Intermediate Certification Authorities: %{issuer}"
+msgstr ""
+
+#: src/views/other/Install.vue:18
+msgid "Invalid E-mail!"
+msgstr ""
+
+#: src/views/user/User.vue:25
+msgid "Leave blank for no change"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:11
+msgid "Load Averages:"
+msgstr ""
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:5
+msgid "Location"
+msgstr ""
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:2
+msgid "Locations"
+msgstr ""
+
+#: src/routes/index.ts:99
+#: src/views/other/Login.vue:37
+msgid "Login"
+msgstr ""
+
+#: src/views/other/Login.vue:79
+msgid "Login successful"
+msgstr ""
+
+#: src/views/layouts/HeaderLayout.vue:29
+msgid "Logout successful"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:34
+msgid "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate."
+msgstr ""
+
+#: src/routes/index.ts:58
+msgid "Manage Configs"
+msgstr ""
+
+#: src/routes/index.ts:33
+#: src/views/domain/DomainList.vue:2
+msgid "Manage Sites"
+msgstr ""
+
+#: src/routes/index.ts:25
+msgid "Manage Users"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:32
+msgid "Memory"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:29
+msgid "Memory and Storage"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:62
+msgid "Modify Config"
+msgstr ""
+
+#: src/views/config/Config.vue:24
+#: src/views/domain/DomainList.vue:32
+msgid "Name"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:74
+msgid "Network"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:48
+msgid "Network Statistics"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:52
+msgid "Network Total Receive"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:56
+msgid "Network Total Send"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:51
+msgid "Next"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:9
+msgid "No"
+msgstr ""
+
+#: src/routes/index.ts:111
+msgid "Not Found"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:8
+msgid "Not Valid Before: %{date}"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:26
+msgid "Note: The server_name in the current configuration must be the domain name you need to get the certificate."
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:17
+msgid "OS:"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:15
+msgid "Params"
+msgstr ""
+
+#: src/views/other/Login.vue:30
+#: src/views/user/User.vue:19
+msgid "Password"
+msgstr ""
+
+#: src/views/other/Install.vue:48
+msgid "Password (*)"
+msgstr ""
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:21
+#: src/views/domain/ngx_conf/LocationEditor.vue:9
+msgid "Path"
+msgstr ""
+
+#: src/views/other/Install.vue:22
+msgid "Please input your E-mail!"
+msgstr ""
+
+#: src/views/other/Install.vue:45
+#: src/views/other/Login.vue:27
+msgid "Please input your password!"
+msgstr ""
+
+#: src/views/other/Install.vue:34
+#: src/views/other/Login.vue:16
+msgid "Please input your username!"
+msgstr ""
+
+#: src/views/other/About.vue:9
+msgid "Project Team"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:107
+#: src/views/dashboard/DashBoard.vue:183
+msgid "Reads"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:173
+#: src/views/dashboard/DashBoard.vue:78
+msgid "Receive"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:10
+#: src/views/domain/DomainEdit.vue:50
+msgid "Save"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:21
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:22
+msgid "Save Directive"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:64
+#: src/views/domain/DomainAdd.vue:117
+#: src/views/domain/DomainEdit.vue:148
+msgid "Save error %{msg}"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:61
+#: src/views/domain/DomainAdd.vue:107
+#: src/views/domain/DomainEdit.vue:143
+msgid "Saved successfully"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:176
+#: src/views/dashboard/DashBoard.vue:85
+msgid "Send"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:52
+#: src/views/domain/DomainEdit.vue:110
+#: src/views/domain/DomainEdit.vue:121
+#: src/views/domain/DomainEdit.vue:129
+#: src/views/other/Login.vue:84
+msgid "Server error"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:5
+msgid "Server Info"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:74
+msgid "server_name not found in directives"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/DomainAdd.vue:26
+msgid "server_name parameter is required"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:20
+#: src/views/domain/cert/IssueCert.vue:80
+msgid "server_name parameters more than one"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:6
+msgid "Single Directive"
+msgstr ""
+
+#: src/routes/index.ts:41
+msgid "Sites List"
+msgstr ""
+
+#: src/views/domain/DomainList.vue:38
+msgid "Status"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:41
+msgid "Storage"
+msgstr ""
+
+#: src/views/domain/cert/CertInfo.vue:5
+msgid "Subject Name: %{name}"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:36
+msgid "Swap"
+msgstr ""
+
+#: src/routes/index.ts:75
+#: src/views/pty/Terminal.vue:2
+msgid "Terminal"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:30
+msgid "The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued."
+msgstr ""
+
+#: src/views/other/Install.vue:59
+msgid "The filename cannot contain the following characters: %{c}"
+msgstr ""
+
+#: src/views/config/Config.vue:30
+#: src/views/domain/DomainList.vue:49
+#: src/views/user/User.vue:35
+msgid "Updated at"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:7
+msgid "Uptime:"
+msgstr ""
+
+#: src/views/other/Login.vue:18
+#: src/views/user/User.vue:11
+msgid "Username"
+msgstr ""
+
+#: src/views/other/Install.vue:36
+msgid "Username (*)"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:12
+#: src/views/domain/DomainAdd.vue:21
+msgid "Warning"
+msgstr ""
+
+#: src/views/dashboard/DashBoard.vue:100
+#: src/views/dashboard/DashBoard.vue:180
+msgid "Writes"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:8
+msgid "Yes"
+msgstr ""
+
+#: src/views/other/About.vue:17
+msgctxt "Project"
+msgid "License"
+msgstr ""

Plik diff jest za duży
+ 0 - 0
frontend-next/src/language/translations.json


+ 639 - 0
frontend-next/src/language/zh_CN/app.po

@@ -0,0 +1,639 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: Hintay <hintay@me.com>\n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: easygettext\n"
+"X-Generator: Poedit 2.2\n"
+
+#: src/router/index.js:107
+msgid "404 Not Found"
+msgstr "404 未找到页面"
+
+#: src/router/index.js:85
+msgid "About"
+msgstr "关于"
+
+#: src/views/config/Config.vue:18 src/views/domain/DomainList.vue:29
+#: src/views/user/User.vue:35
+msgid "Action"
+msgstr "操作"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:89
+msgid "Add Directive Below"
+msgstr "在下面添加指令"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:70
+#: src/views/domain/ngx_conf/LocationEditor.vue:120
+msgid "Add Location"
+msgstr "添加 Location"
+
+#: src/router/index.js:47 src/views/domain/DomainAdd.vue:31
+msgid "Add Site"
+msgstr "添加站点"
+
+#: src/views/domain/DomainEdit.vue:57
+msgid "Advance Mode"
+msgstr "高级模式"
+
+#: src/components/StdDataDisplay/StdTable.vue:68
+msgid "Are you sure you want to destroy?"
+msgstr "您确定要删除?"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:46
+msgid "Are you sure you want to remove this directive?"
+msgstr "您确定要删除这条指令?"
+
+#: src/components/StdDataDisplay/StdTable.vue:44
+msgid "Are you sure you want to restore?"
+msgstr "您确定要反删除?"
+
+#: src/views/domain/cert/IssueCert.vue:82
+msgid "Auto-renewal disabled for %{name}"
+msgstr "成功关闭 %{name} 自动续签"
+
+#: src/views/domain/cert/IssueCert.vue:76
+msgid "Auto-renewal enabled for %{name}"
+msgstr "成功启用 %{name} 自动续签"
+
+#: src/views/domain/DomainEdit.vue:41
+msgid "Back"
+msgstr "返回"
+
+#: src/views/domain/DomainAdd.vue:41
+msgid "Base information"
+msgstr "基本信息"
+
+#: src/views/domain/DomainEdit.vue:60
+msgid "Basic Mode"
+msgstr "基本模式"
+
+#: src/views/other/About.vue:11
+msgid "Build with"
+msgstr "构建基于"
+
+#: src/views/config/ConfigEdit.vue:5
+msgid "Cancel"
+msgstr "取消"
+
+#: src/views/domain/cert/CertInfo.vue:12 src/views/domain/cert/CertInfo.vue:2
+msgid "Certificate has expired"
+msgstr "此证书已过期"
+
+#: src/views/domain/cert/CertInfo.vue:16 src/views/domain/cert/CertInfo.vue:2
+msgid "Certificate is valid"
+msgstr "此证书有效"
+
+#: src/views/domain/cert/CertInfo.vue:2
+msgid "Certificate Status"
+msgstr "证书状态"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:33
+#: src/views/domain/ngx_conf/LocationEditor.vue:77
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:65
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:67
+msgid "Comments"
+msgstr "注释"
+
+#: src/views/domain/DomainAdd.vue:55
+msgid "Configuration Name"
+msgstr "配置名称"
+
+#: src/views/config/Config.vue:8
+msgid "Configurations"
+msgstr "配置"
+
+#: src/views/domain/DomainAdd.vue:44
+msgid "Configure SSL"
+msgstr "配置 SSL"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:54
+#: src/views/domain/ngx_conf/LocationEditor.vue:100
+msgid "Content"
+msgstr "内容"
+
+#: src/views/dashboard/DashBoard.vue:211
+msgid "CPU Status"
+msgstr "CPU 状态"
+
+#: src/views/dashboard/DashBoard.vue:22
+msgid "CPU:"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:46 src/views/domain/DomainAdd.vue:5
+msgid "Create Another"
+msgstr "再创建一个"
+
+#: src/views/user/User.vue:23
+msgid "Created at"
+msgstr "创建时间"
+
+#: src/router/index.js:19
+msgid "Dashboard"
+msgstr "仪表盘"
+
+#: src/views/other/Install.vue:105
+msgid "Database (Optional, default: database)"
+msgstr "数据库 (可选,默认: database)"
+
+#: src/components/StdDataDisplay/StdTable.vue:142
+msgid "Delete ID: %{id}"
+msgstr "删除 ID: %{id}"
+
+#: src/components/StdDataDisplay/StdTable.vue:74
+msgid "Destroy"
+msgstr "删除"
+
+#: src/router/index.js:133
+msgid "Detected version update, this page will refresh."
+msgstr "检测到版本更新,页面将会刷新。"
+
+#: src/views/other/About.vue:11
+msgid "Development Mode"
+msgstr "开发模式"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:57
+msgid "Directive"
+msgstr "指令"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:20
+msgid "Directives"
+msgstr "指令"
+
+#: src/views/domain/cert/IssueCert.vue:84
+msgid "Disable auto-renewal failed for %{name}"
+msgstr "关闭 %{name} 自动续签失败"
+
+#: src/views/domain/DomainEdit.vue:43 src/views/domain/DomainList.vue:17
+#: src/views/domain/DomainList.vue:32
+msgid "Disabled"
+msgstr "禁用"
+
+#: src/views/domain/DomainEdit.vue:106 src/views/domain/DomainList.vue:55
+msgid "Disabled successfully"
+msgstr "禁用成功"
+
+#: src/views/dashboard/DashBoard.vue:289
+msgid "Disk IO"
+msgstr "磁盘 IO"
+
+#: src/views/domain/DomainAdd.vue:125
+msgid "Domain Config Created Successfully"
+msgstr "域名配置文件创建成功"
+
+#: src/components/StdDataDisplay/StdTable.vue:38
+msgid "Edit"
+msgstr "编辑"
+
+#: src/views/domain/DomainEdit.vue:27
+msgid "Edit %{n}"
+msgstr "编辑 %{n}"
+
+#: src/router/index.js:69 src/views/config/ConfigEdit.vue:15
+msgid "Edit Configuration"
+msgstr "编辑配置"
+
+#: src/router/index.js:51
+msgid "Edit Site"
+msgstr "编辑站点"
+
+#: src/views/other/Install.vue:31
+msgid "Email (*)"
+msgstr "邮箱 (*)"
+
+#: src/views/domain/cert/IssueCert.vue:78
+msgid "Enable auto-renewal failed for %{name}"
+msgstr "启用 %{name} 自动续签失败"
+
+#: src/views/domain/DomainAdd.vue:39
+msgid "Enable failed"
+msgstr "启用失败"
+
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:20
+msgid "Enable TLS"
+msgstr "启用 TLS"
+
+#: src/views/domain/DomainEdit.vue:34 src/views/domain/DomainEdit.vue:75
+#: src/views/domain/DomainList.vue:16 src/views/domain/DomainList.vue:36
+msgid "Enabled"
+msgstr "启用"
+
+#: src/views/domain/DomainAdd.vue:36 src/views/domain/DomainEdit.vue:98
+#: src/views/domain/DomainList.vue:46
+msgid "Enabled successfully"
+msgstr "启用成功"
+
+#: src/views/domain/cert/IssueCert.vue:36
+msgid "Encrypt website with Let's Encrypt"
+msgstr "用 Let's Encrypt 对网站进行加密"
+
+#: src/views/domain/cert/CertInfo.vue:5
+msgid "Expiration Date: %{date}"
+msgstr "过期时间: %{date}"
+
+#: src/views/domain/DomainEdit.vue:109 src/views/domain/DomainList.vue:59
+msgid "Failed to disable %{msg}"
+msgstr "禁用失败 %{msg}"
+
+#: src/views/domain/DomainEdit.vue:101 src/views/domain/DomainList.vue:50
+msgid "Failed to enable %{msg}"
+msgstr "启用失败 %{msg}"
+
+#: src/views/other/Error.vue:9
+msgid "File Not Found"
+msgstr "未找到文件"
+
+#: src/views/domain/DomainAdd.vue:47
+msgid "Finished"
+msgstr "完成"
+
+#: src/views/domain/methods.js:6
+msgid "Getting the certificate, please wait..."
+msgstr "正在获取证书,请稍等..."
+
+#: src/router/index.js:12
+msgid "Home"
+msgstr "首页"
+
+#: src/router/index.js:95 src/views/other/Install.vue:51
+msgid "Install"
+msgstr "安装"
+
+#: src/views/domain/cert/CertInfo.vue:3
+msgid "Intermediate Certification Authorities: %{issuer}"
+msgstr "中级证书颁发机构: %{issuer}"
+
+#: src/views/other/Install.vue:46
+msgid "Invalid E-mail!"
+msgstr "无效的邮箱!"
+
+#: src/views/user/User.vue:19
+msgid "Leave blank for no change"
+msgstr "留空表示不修改"
+
+#: src/views/other/About.vue:16
+msgctxt "Project"
+msgid "License"
+msgstr "开源许可"
+
+#: src/views/dashboard/DashBoard.vue:10
+msgid "Load Averages:"
+msgstr "系统负载:"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:26
+msgid "Location"
+msgstr "Location"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:16
+msgid "Locations"
+msgstr "Locations"
+
+#: src/router/index.js:101 src/views/other/Login.vue:25
+msgid "Login"
+msgstr "登录"
+
+#: src/views/other/Login.vue:30
+msgid "Login successful"
+msgstr "登录成功"
+
+#: src/layouts/HeaderLayout.vue:9
+msgid "Logout successful"
+msgstr "登出成功"
+
+#: src/views/domain/cert/IssueCert.vue:23
+msgid ""
+"Make sure you have configured a reverse proxy for .well-known\n"
+"            directory to HTTPChallengePort (default: 9180) before getting "
+"the certificate."
+msgstr ""
+"在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 "
+"HTTPChallengePort (默认: 9180)"
+
+#: src/router/index.js:60
+msgid "Manage Configs"
+msgstr "配置管理"
+
+#: src/router/index.js:35 src/views/domain/DomainList.vue:12
+msgid "Manage Sites"
+msgstr "网站管理"
+
+#: src/router/index.js:27
+msgid "Manage Users"
+msgstr "用户管理"
+
+#: src/views/dashboard/DashBoard.vue:105
+msgid "Memory"
+msgstr "内存"
+
+#: src/views/dashboard/DashBoard.vue:93
+msgid "Memory and Storage"
+msgstr "内存与存储"
+
+#: src/views/domain/DomainAdd.vue:43 src/views/domain/DomainAdd.vue:2
+msgid "Modify Config"
+msgstr "修改配置文件"
+
+#: src/views/config/Config.vue:5 src/views/domain/DomainList.vue:5
+msgid "Name"
+msgstr "名称"
+
+#: src/views/dashboard/DashBoard.vue:238
+msgid "Network"
+msgstr "网络"
+
+#: src/views/dashboard/DashBoard.vue:163
+msgid "Network Statistics"
+msgstr "流量统计"
+
+#: src/views/dashboard/DashBoard.vue:172
+msgid "Network Total Receive"
+msgstr "下载流量"
+
+#: src/views/dashboard/DashBoard.vue:181
+msgid "Network Total Send"
+msgstr "上传流量"
+
+#: src/views/domain/DomainAdd.vue:36
+msgid "Next"
+msgstr "下一步"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:48
+msgid "No"
+msgstr "取消"
+
+#: src/components/StdDataDisplay/StdTable.vue:62
+msgid "No, I'm rethink"
+msgstr "再想想"
+
+#: src/router/index.js:113
+msgid "Not Found"
+msgstr "找不到页面"
+
+#: src/views/domain/cert/CertInfo.vue:7
+msgid "Not Valid Before: %{date}"
+msgstr "此前无效: %{date}"
+
+#: src/views/domain/cert/IssueCert.vue:15
+msgid ""
+"Note: The server_name in the current configuration must be the domain name\n"
+"            you need to get the certificate."
+msgstr "注意:当前配置中的 server_name 必须为需要申请证书的域名。"
+
+#: src/router/index.js:137
+msgid "OK"
+msgstr "确定"
+
+#: src/views/dashboard/DashBoard.vue:16
+msgid "OS:"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:63
+msgid "Params"
+msgstr "参数"
+
+#: src/views/other/Login.vue:56 src/views/user/User.vue:13
+msgid "Password"
+msgstr "密码"
+
+#: src/views/other/Install.vue:83
+msgid "Password (*)"
+msgstr "密码 (*)"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:42
+#: src/views/domain/ngx_conf/LocationEditor.vue:88
+msgid "Path"
+msgstr "路径"
+
+#: src/views/other/Install.vue:50
+msgid "Please input your E-mail!"
+msgstr "请输入您的邮箱!"
+
+#: src/views/other/Install.vue:96 src/views/other/Login.vue:69
+msgid "Please input your password!"
+msgstr "请输入您的密码!"
+
+#: src/views/other/Install.vue:73 src/views/other/Login.vue:46
+msgid "Please input your username!"
+msgstr "请输入您的用户名!"
+
+#: src/views/other/About.vue:8
+msgid "Project Team"
+msgstr "项目团队"
+
+#: src/views/dashboard/DashBoard.vue:61 src/views/dashboard/DashBoard.vue:312
+msgid "Reads"
+msgstr "读"
+
+#: src/views/dashboard/DashBoard.vue:51 src/views/dashboard/DashBoard.vue:247
+msgid "Receive"
+msgstr "下载"
+
+#: src/components/StdDataDisplay/StdTable.vue:50
+msgid "Restore"
+msgstr "反删除"
+
+#: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainEdit.vue:44
+msgid "Save"
+msgstr "保存"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:101
+msgid "Save Directive"
+msgstr "保存指令"
+
+#: src/views/config/ConfigEdit.vue:47 src/views/domain/DomainAdd.vue:43
+#: src/views/domain/DomainEdit.vue:90
+msgid "Save error %{msg}"
+msgstr "保存错误 %{msg}"
+
+#: src/views/config/ConfigEdit.vue:44 src/views/domain/DomainAdd.vue:33
+#: src/views/domain/DomainEdit.vue:85
+msgid "Saved successfully"
+msgstr "保存成功"
+
+#: src/views/dashboard/DashBoard.vue:54 src/views/dashboard/DashBoard.vue:261
+msgid "Send"
+msgstr "上传"
+
+#: src/components/StdDataDisplay/StdTable.vue:145
+#: src/views/config/ConfigEdit.vue:35 src/views/domain/DomainEdit.vue:52
+#: src/views/domain/DomainEdit.vue:63 src/views/domain/DomainEdit.vue:71
+#: src/views/other/Login.vue:35
+msgid "Server error"
+msgstr "服务器错误"
+
+#: src/views/dashboard/DashBoard.vue:38
+msgid "Server Info"
+msgstr "服务器信息"
+
+#: src/views/domain/cert/IssueCert.vue:33
+msgid "server_name not found in directives"
+msgstr "未在指令集合中找到 server_name"
+
+#: src/views/domain/DomainAdd.vue:20 src/views/domain/DomainAdd.vue:11
+#: src/views/domain/DomainAdd.vue:1 src/views/domain/cert/IssueCert.vue:6
+#: src/views/domain/cert/IssueCert.vue:1
+msgid "server_name parameter is required"
+msgstr "必须为 server_name 指令指明参数"
+
+#: src/views/domain/cert/IssueCert.vue:9 src/views/domain/cert/IssueCert.vue:4
+#: src/views/domain/cert/IssueCert.vue:39
+msgid "server_name parameters more than one"
+msgstr "server_name 指令包含多个参数"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:30
+msgid "Single Directive"
+msgstr "单行指令"
+
+#: src/router/index.js:43
+msgid "Sites List"
+msgstr "站点列表"
+
+#: src/views/domain/DomainList.vue:11
+msgid "Status"
+msgstr "状态"
+
+#: src/views/dashboard/DashBoard.vue:137
+msgid "Storage"
+msgstr "存储"
+
+#: src/views/domain/cert/CertInfo.vue:4
+msgid "Subject Name: %{name}"
+msgstr "主体名称: %{name}"
+
+#: src/views/dashboard/DashBoard.vue:121
+msgid "Swap"
+msgstr ""
+
+#: src/router/index.js:132
+msgid "System message"
+msgstr "系统消息"
+
+#: src/router/index.js:77 src/views/pty/Terminal.vue:12
+msgid "Terminal"
+msgstr "终端"
+
+#: src/views/domain/cert/IssueCert.vue:19
+msgid ""
+"The certificate for the domain will be checked every hour,\n"
+"            and will be renewed if it has been more than 1 month since it "
+"was last issued."
+msgstr ""
+"系统将会每小时检测一次该域名证书,若距离上次签发已超过1个月,则将自动续签。"
+
+#: src/views/other/Install.vue:120
+msgid "The filename cannot contain the following characters: %{c}"
+msgstr "文件名不能包含以下字符: %{c}"
+
+#: src/views/config/Config.vue:11 src/views/domain/DomainList.vue:22
+#: src/views/user/User.vue:29
+msgid "Updated at"
+msgstr "修改时间"
+
+#: src/views/dashboard/DashBoard.vue:6
+msgid "Uptime:"
+msgstr "运行时间:"
+
+#: src/views/other/Login.vue:33 src/views/user/User.vue:5
+msgid "Username"
+msgstr "用户名"
+
+#: src/views/other/Install.vue:60
+msgid "Username (*)"
+msgstr "用户名 (*)"
+
+#: src/views/domain/DomainAdd.vue:74 src/views/domain/cert/IssueCert.vue:49
+msgid "Warning"
+msgstr "警告"
+
+#: src/views/dashboard/DashBoard.vue:58 src/views/dashboard/DashBoard.vue:298
+msgid "Writes"
+msgstr "写"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:47
+msgid "Yes"
+msgstr "是的"
+
+#: src/components/StdDataDisplay/StdTable.vue:56
+msgid "Yes, I'm sure"
+msgstr "是的"
+
+#~ msgid "Certificate Auto-renewal"
+#~ msgstr "证书自动续签"
+
+#~ msgid "Certificate Path (ssl_certificate)"
+#~ msgstr "TLS 证书路径 (ssl_certificate)"
+
+#~ msgid "HTTP Listen Port"
+#~ msgstr "HTTP 监听端口"
+
+#~ msgid "HTTPS Listen Port"
+#~ msgstr "HTTPS 监听端口"
+
+#~ msgid "Index (index)"
+#~ msgstr "网站首页 (index)"
+
+#~ msgid "Private Key Path (ssl_certificate_key)"
+#~ msgstr "私钥路径 (ssl_certificate_key)"
+
+#~ msgid "Root Directory (root)"
+#~ msgstr "网站根目录 (root)"
+
+#~ msgid "Server Names (server_name)"
+#~ msgstr "网站域名 (server_name)"
+
+#~ msgid ""
+#~ "The certificate for the domain will be checked every hour, and will be "
+#~ "renewed if it has been more than 1 month since it was last issued.<br/>If "
+#~ "you do not have a certificate before, please click \"Getting Certificate "
+#~ "from Let's Encrypt\" first."
+#~ msgstr ""
+#~ "系统将会每小时检测一次该域名证书,若距离上次签发已超过1个月,则将自动续"
+#~ "签。<br/>如果您之前没有证书,请先点击 \"从 Let's Encrypt 获取证书\"。"
+
+#~ msgid "Do you want to change the template to support the TLS?"
+#~ msgstr "你想要改变模板以支持 TLS 吗?"
+
+#~ msgid "Edit Configuration File"
+#~ msgstr "编辑配置文件"
+
+#~ msgid "Getting Certificate from Let's Encrypt"
+#~ msgstr "从 Let's Encrypt 获取证书"
+
+#~ msgid "Skip"
+#~ msgstr "跳过"
+
+#~ msgid ""
+#~ "The following values will only take effect if you have the corresponding "
+#~ "fields in your configuration file. The configuration filename cannot be "
+#~ "changed after it has been created."
+#~ msgstr ""
+#~ "只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不"
+#~ "可修改。"
+
+#~ msgid "This feature is not available in demo."
+#~ msgstr "该功能在 Demo 中不可用。"
+
+#~ msgid "This operation will lose the custom configuration."
+#~ msgstr "该操作将会丢失自定义配置。"
+
+#~ msgid ""
+#~ "Add site here first, then you can configure TLS on the domain edit page."
+#~ msgstr "在这里添加站点,完成后可进入编辑页面配置 TLS。"
+
+#~ msgid "Server Status"
+#~ msgstr "服务器状态"
+
+#~ msgid "Used: %{u}, Cached: %{c}, Free: %{f}, Physical Memory: %{p}"
+#~ msgstr "已使用: %{u}, 缓存: %{c}, 空闲: %{f}, 物理内存: %{p}"
+
+#~ msgid "Used: %{used} / Total: %{total}"
+#~ msgstr "已使用: %{used} / 总共: %{total}"
+
+#~ msgid "CPU"
+#~ msgstr "CPU"

+ 642 - 0
frontend-next/src/language/zh_TW/app.po

@@ -0,0 +1,642 @@
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: Hintay <hintay@me.com>\n"
+"Language-Team: none\n"
+"Language: zh_TW\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: easygettext\n"
+"X-Generator: Poedit 2.2\n"
+
+#: src/router/index.js:107
+msgid "404 Not Found"
+msgstr "404 未找到頁面"
+
+#: src/router/index.js:85
+msgid "About"
+msgstr "關於"
+
+#: src/views/config/Config.vue:18 src/views/domain/DomainList.vue:29
+#: src/views/user/User.vue:35
+msgid "Action"
+msgstr "操作"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:89
+msgid "Add Directive Below"
+msgstr "在下面新增指令"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:70
+#: src/views/domain/ngx_conf/LocationEditor.vue:120
+msgid "Add Location"
+msgstr "新增 Location"
+
+#: src/router/index.js:47 src/views/domain/DomainAdd.vue:32
+msgid "Add Site"
+msgstr "新增站點"
+
+#: src/views/domain/DomainEdit.vue:57
+msgid "Advance Mode"
+msgstr "高階模式"
+
+#: src/components/StdDataDisplay/StdTable.vue:68
+msgid "Are you sure you want to destroy?"
+msgstr "您确定要删除?"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:46
+msgid "Are you sure you want to remove this directive?"
+msgstr "您確定要刪除這條指令?"
+
+#: src/components/StdDataDisplay/StdTable.vue:44
+msgid "Are you sure you want to restore?"
+msgstr "您确定要恢復?"
+
+#: src/views/domain/cert/IssueCert.vue:82
+msgid "Auto-renewal disabled for %{name}"
+msgstr "已關閉 %{name} 自動續簽"
+
+#: src/views/domain/cert/IssueCert.vue:76
+msgid "Auto-renewal enabled for %{name}"
+msgstr "已啟用 %{name} 自動續簽"
+
+#: src/views/domain/DomainEdit.vue:41
+msgid "Back"
+msgstr "返回"
+
+#: src/views/domain/DomainAdd.vue:42
+msgid "Base information"
+msgstr "基本訊息"
+
+#: src/views/domain/DomainEdit.vue:60
+msgid "Basic Mode"
+msgstr "基本模式"
+
+#: src/views/other/About.vue:11
+msgid "Build with"
+msgstr "構建基於"
+
+#: src/views/config/ConfigEdit.vue:5
+msgid "Cancel"
+msgstr "取消"
+
+#: src/views/domain/cert/CertInfo.vue:12 src/views/domain/cert/CertInfo.vue:2
+msgid "Certificate has expired"
+msgstr "此憑證已過期"
+
+#: src/views/domain/cert/CertInfo.vue:16 src/views/domain/cert/CertInfo.vue:2
+msgid "Certificate is valid"
+msgstr "此憑證有效"
+
+#: src/views/domain/cert/CertInfo.vue:2
+msgid "Certificate Status"
+msgstr "憑證狀態"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:33
+#: src/views/domain/ngx_conf/LocationEditor.vue:77
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:67
+msgid "Comments"
+msgstr "註釋"
+
+#: src/views/domain/DomainAdd.vue:56
+msgid "Configuration Name"
+msgstr "配置名稱"
+
+#: src/views/config/Config.vue:8
+msgid "Configurations"
+msgstr "配置"
+
+#: src/views/domain/DomainAdd.vue:45
+msgid "Configure SSL"
+msgstr "配置 SSL"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:54
+#: src/views/domain/ngx_conf/LocationEditor.vue:100
+msgid "Content"
+msgstr "內容"
+
+#: src/views/dashboard/DashBoard.vue:211
+msgid "CPU Status"
+msgstr "中央處理器狀態"
+
+#: src/views/dashboard/DashBoard.vue:22
+msgid "CPU:"
+msgstr "中央處理器:"
+
+#: src/views/domain/DomainAdd.vue:50 src/views/domain/DomainAdd.vue:5
+msgid "Create Another"
+msgstr "再創建一個"
+
+#: src/views/user/User.vue:23
+msgid "Created at"
+msgstr "建立時間"
+
+#: src/router/index.js:19
+msgid "Dashboard"
+msgstr "儀表盤"
+
+#: src/views/other/Install.vue:105
+msgid "Database (Optional, default: database)"
+msgstr "資料庫 (可選,預設: database)"
+
+#: src/components/StdDataDisplay/StdTable.vue:142
+msgid "Delete ID: %{id}"
+msgstr "刪除 ID: %{id}"
+
+#: src/components/StdDataDisplay/StdTable.vue:74
+msgid "Destroy"
+msgstr "删除"
+
+#: src/router/index.js:133
+msgid "Detected version update, this page will refresh."
+msgstr "檢測到版本更新,頁面將會重新整理。"
+
+#: src/views/other/About.vue:11
+msgid "Development Mode"
+msgstr "開發模式"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:57
+msgid "Directive"
+msgstr "指令"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:20
+msgid "Directives"
+msgstr "指令"
+
+#: src/views/domain/cert/IssueCert.vue:84
+msgid "Disable auto-renewal failed for %{name}"
+msgstr "關閉 %{name} 自動續簽失敗"
+
+#: src/views/domain/DomainEdit.vue:43 src/views/domain/DomainList.vue:17
+#: src/views/domain/DomainList.vue:32
+msgid "Disabled"
+msgstr "禁用"
+
+#: src/views/domain/DomainEdit.vue:106 src/views/domain/DomainList.vue:55
+msgid "Disabled successfully"
+msgstr "禁用成功"
+
+#: src/views/dashboard/DashBoard.vue:289
+msgid "Disk IO"
+msgstr "磁碟 IO"
+
+#: src/views/domain/DomainAdd.vue:135
+msgid "Domain Config Created Successfully"
+msgstr "域名配置文件創建成功"
+
+#: src/components/StdDataDisplay/StdTable.vue:38
+msgid "Edit"
+msgstr "编辑"
+
+#: src/views/domain/DomainEdit.vue:27
+msgid "Edit %{n}"
+msgstr "編輯 %{n}"
+
+#: src/router/index.js:69 src/views/config/ConfigEdit.vue:15
+msgid "Edit Configuration"
+msgstr "編輯配置"
+
+#: src/router/index.js:51
+msgid "Edit Site"
+msgstr "編輯站點"
+
+#: src/views/other/Install.vue:31
+msgid "Email (*)"
+msgstr "郵箱 (*)"
+
+#: src/views/domain/cert/IssueCert.vue:78
+msgid "Enable auto-renewal failed for %{name}"
+msgstr "啟用 %{name} 自動續簽失敗"
+
+#: src/views/domain/DomainAdd.vue:39
+msgid "Enable failed"
+msgstr "啟用失敗"
+
+#: src/views/domain/DomainAdd.vue:94
+msgid "Enable TLS"
+msgstr "啟用 TLS"
+
+#: src/views/domain/DomainEdit.vue:34 src/views/domain/DomainEdit.vue:75
+#: src/views/domain/DomainList.vue:16 src/views/domain/DomainList.vue:36
+msgid "Enabled"
+msgstr "啟用"
+
+#: src/views/domain/DomainAdd.vue:36 src/views/domain/DomainEdit.vue:98
+#: src/views/domain/DomainList.vue:46
+msgid "Enabled successfully"
+msgstr "啟用成功"
+
+#: src/views/domain/cert/IssueCert.vue:36
+msgid "Encrypt website with Let's Encrypt"
+msgstr "用 Let's Encrypt 對網站進行加密"
+
+#: src/views/domain/cert/CertInfo.vue:5
+msgid "Expiration Date: %{date}"
+msgstr "過期時間: %{date}"
+
+#: src/views/domain/DomainEdit.vue:109 src/views/domain/DomainList.vue:59
+msgid "Failed to disable %{msg}"
+msgstr "禁用失敗 %{msg}"
+
+#: src/views/domain/DomainEdit.vue:101 src/views/domain/DomainList.vue:50
+msgid "Failed to enable %{msg}"
+msgstr "啟用失敗 %{msg}"
+
+#: src/views/other/Error.vue:9
+msgid "File Not Found"
+msgstr "未找到檔案"
+
+#: src/views/domain/DomainAdd.vue:48
+msgid "Finished"
+msgstr "完成"
+
+#: src/views/domain/methods.js:6
+msgid "Getting the certificate, please wait..."
+msgstr "正在獲取憑證,請稍等..."
+
+#: src/router/index.js:12
+msgid "Home"
+msgstr "首頁"
+
+#: src/router/index.js:95 src/views/other/Install.vue:51
+msgid "Install"
+msgstr "安裝"
+
+#: src/views/domain/cert/CertInfo.vue:3
+msgid "Intermediate Certification Authorities: %{issuer}"
+msgstr "中級憑證頒發機構: %{issuer}"
+
+#: src/views/other/Install.vue:46
+msgid "Invalid E-mail!"
+msgstr "無效的郵箱!"
+
+#: src/views/user/User.vue:19
+msgid "Leave blank for no change"
+msgstr "留空表示不修改"
+
+#: src/views/other/About.vue:16
+msgctxt "Project"
+msgid "License"
+msgstr "開源軟體授權條款"
+
+#: src/views/dashboard/DashBoard.vue:10
+msgid "Load Averages:"
+msgstr "系統負載:"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:26
+msgid "Location"
+msgstr "Location"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:16
+msgid "Locations"
+msgstr "Locations"
+
+#: src/router/index.js:101 src/views/other/Login.vue:25
+msgid "Login"
+msgstr "登入"
+
+#: src/views/other/Login.vue:30
+msgid "Login successful"
+msgstr "登入成功"
+
+#: src/layouts/HeaderLayout.vue:9
+msgid "Logout successful"
+msgstr "登出成功"
+
+#: src/views/domain/cert/IssueCert.vue:19
+msgid ""
+"Make sure you have configured a reverse proxy for .well-known\n"
+"            directory to HTTPChallengePort (default: 9180) before getting "
+"the certificate."
+msgstr ""
+"在獲取憑證前,請確保配置檔案中已將 .well-known 目錄反向代理到 "
+"HTTPChallengePort (預設: 9180)"
+
+#: src/router/index.js:60
+msgid "Manage Configs"
+msgstr "配置管理"
+
+#: src/router/index.js:35 src/views/domain/DomainList.vue:12
+msgid "Manage Sites"
+msgstr "網站管理"
+
+#: src/router/index.js:27
+msgid "Manage Users"
+msgstr "使用者管理"
+
+#: src/views/dashboard/DashBoard.vue:105
+msgid "Memory"
+msgstr "記憶體"
+
+#: src/views/dashboard/DashBoard.vue:93
+msgid "Memory and Storage"
+msgstr "記憶體和存儲"
+
+#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainAdd.vue:2
+msgid "Modify Config"
+msgstr "修改配置"
+
+#: src/views/config/Config.vue:5 src/views/domain/DomainList.vue:5
+msgid "Name"
+msgstr "名稱"
+
+#: src/views/dashboard/DashBoard.vue:238
+msgid "Network"
+msgstr "網路"
+
+#: src/views/dashboard/DashBoard.vue:163
+msgid "Network Statistics"
+msgstr "網路統計"
+
+#: src/views/dashboard/DashBoard.vue:172
+msgid "Network Total Receive"
+msgstr "下載流量"
+
+#: src/views/dashboard/DashBoard.vue:181
+msgid "Network Total Send"
+msgstr "上傳流量"
+
+#: src/views/domain/DomainAdd.vue:40
+msgid "Next"
+msgstr "下一步"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:48
+msgid "No"
+msgstr "取消"
+
+#: src/components/StdDataDisplay/StdTable.vue:62
+msgid "No, I'm rethink"
+msgstr "再想想"
+
+#: src/router/index.js:113
+msgid "Not Found"
+msgstr "找不到頁面"
+
+#: src/views/domain/cert/CertInfo.vue:7
+msgid "Not Valid Before: %{date}"
+msgstr "此前無效: %{date}"
+
+#: src/views/domain/cert/IssueCert.vue:15
+msgid ""
+"Note: The server_name in the current configuration must be the domain name "
+"you need to get the\n"
+"            certificate."
+msgstr "注意:當前配置中的 server_name 必須為需要申請憑證的域名。"
+
+#: src/router/index.js:137
+msgid "OK"
+msgstr "確定"
+
+#: src/views/dashboard/DashBoard.vue:16
+msgid "OS:"
+msgstr "作業系統:"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:63
+msgid "Params"
+msgstr "參數"
+
+#: src/views/other/Login.vue:56 src/views/user/User.vue:13
+msgid "Password"
+msgstr "密碼"
+
+#: src/views/other/Install.vue:83
+msgid "Password (*)"
+msgstr "密碼 (*)"
+
+#: src/views/domain/ngx_conf/LocationEditor.vue:42
+#: src/views/domain/ngx_conf/LocationEditor.vue:88
+msgid "Path"
+msgstr "路徑"
+
+#: src/views/other/Install.vue:50
+msgid "Please input your E-mail!"
+msgstr "請輸入您的郵箱!"
+
+#: src/views/other/Install.vue:96 src/views/other/Login.vue:69
+msgid "Please input your password!"
+msgstr "請輸入您的密碼!"
+
+#: src/views/other/Install.vue:73 src/views/other/Login.vue:46
+msgid "Please input your username!"
+msgstr "請輸入您的使用者名稱!"
+
+#: src/views/other/About.vue:8
+msgid "Project Team"
+msgstr "專案團隊"
+
+#: src/views/dashboard/DashBoard.vue:61 src/views/dashboard/DashBoard.vue:312
+msgid "Reads"
+msgstr "讀"
+
+#: src/views/dashboard/DashBoard.vue:51 src/views/dashboard/DashBoard.vue:247
+msgid "Receive"
+msgstr "下載"
+
+#: src/components/StdDataDisplay/StdTable.vue:50
+msgid "Restore"
+msgstr "恢復"
+
+#: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainEdit.vue:44
+msgid "Save"
+msgstr "儲存"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:101
+msgid "Save Directive"
+msgstr "儲存指令"
+
+#: src/views/config/ConfigEdit.vue:47 src/views/domain/DomainAdd.vue:43
+#: src/views/domain/DomainEdit.vue:90
+msgid "Save error %{msg}"
+msgstr "儲存錯誤 %{msg}"
+
+#: src/views/config/ConfigEdit.vue:44 src/views/domain/DomainAdd.vue:33
+#: src/views/domain/DomainEdit.vue:85
+msgid "Saved successfully"
+msgstr "儲存成功"
+
+#: src/views/dashboard/DashBoard.vue:54 src/views/dashboard/DashBoard.vue:261
+msgid "Send"
+msgstr "上傳"
+
+#: src/components/StdDataDisplay/StdTable.vue:145
+#: src/views/config/ConfigEdit.vue:35 src/views/domain/DomainEdit.vue:52
+#: src/views/domain/DomainEdit.vue:63 src/views/domain/DomainEdit.vue:71
+#: src/views/other/Login.vue:35
+msgid "Server error"
+msgstr "伺服器錯誤"
+
+#: src/views/dashboard/DashBoard.vue:38
+msgid "Server Info"
+msgstr "伺服器資訊"
+
+#: src/views/domain/cert/IssueCert.vue:33
+msgid "server_name not found in directives"
+msgstr "未在指令集合中找到 server_name"
+
+#: src/views/domain/DomainAdd.vue:20 src/views/domain/DomainAdd.vue:11
+#: src/views/domain/DomainAdd.vue:1 src/views/domain/cert/IssueCert.vue:6
+#: src/views/domain/cert/IssueCert.vue:1
+msgid "server_name parameter is required"
+msgstr "必須為 server_name 指令指明參數"
+
+#: src/views/domain/cert/IssueCert.vue:9 src/views/domain/cert/IssueCert.vue:4
+#: src/views/domain/cert/IssueCert.vue:39
+msgid "server_name parameters more than one"
+msgstr "server_name 指令包含多個參數"
+
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:30
+msgid "Single Directive"
+msgstr "單行指令"
+
+#: src/router/index.js:43
+msgid "Sites List"
+msgstr "站點列表"
+
+#: src/views/domain/DomainList.vue:11
+msgid "Status"
+msgstr "狀態"
+
+#: src/views/dashboard/DashBoard.vue:137
+msgid "Storage"
+msgstr "儲存"
+
+#: src/views/domain/cert/CertInfo.vue:4
+msgid "Subject Name: %{name}"
+msgstr "主體名稱: %{name}"
+
+#: src/views/dashboard/DashBoard.vue:121
+msgid "Swap"
+msgstr "交換空間"
+
+#: src/router/index.js:132
+msgid "System message"
+msgstr "系統訊息"
+
+#: src/router/index.js:77 src/views/pty/Terminal.vue:12
+msgid "Terminal"
+msgstr "終端"
+
+#: src/views/domain/cert/IssueCert.vue:17
+msgid ""
+"The certificate for the domain will be checked every hour,\n"
+"            and will be renewed if it has been more than 1 month since it "
+"was last issued."
+msgstr ""
+"系統將會每小時檢測一次該域名憑證,若距離上次簽發已超過1個月,則將自動續簽。"
+"<br/>如果您之前沒有憑證,請先點選「從 Let's Encrypt 獲取憑證」。"
+
+#: src/views/other/Install.vue:120
+msgid "The filename cannot contain the following characters: %{c}"
+msgstr "檔名不能包含以下字元: %{c}"
+
+#: src/views/config/Config.vue:11 src/views/domain/DomainList.vue:22
+#: src/views/user/User.vue:29
+msgid "Updated at"
+msgstr "修改時間"
+
+#: src/views/dashboard/DashBoard.vue:6
+msgid "Uptime:"
+msgstr "執行時間:"
+
+#: src/views/other/Login.vue:33 src/views/user/User.vue:5
+msgid "Username"
+msgstr "使用者名稱"
+
+#: src/views/other/Install.vue:60
+msgid "Username (*)"
+msgstr "使用者名稱 (*)"
+
+#: src/views/domain/DomainAdd.vue:75 src/views/domain/cert/IssueCert.vue:49
+msgid "Warning"
+msgstr "警告"
+
+#: src/views/dashboard/DashBoard.vue:58 src/views/dashboard/DashBoard.vue:298
+msgid "Writes"
+msgstr "寫"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:47
+msgid "Yes"
+msgstr "是的"
+
+#: src/components/StdDataDisplay/StdTable.vue:56
+msgid "Yes, I'm sure"
+msgstr "是的"
+
+#~ msgid "Certificate Auto-renewal"
+#~ msgstr "證書自動續簽"
+
+#~ msgid "Certificate Path (ssl_certificate)"
+#~ msgstr "TLS 證書路徑 (ssl_certificate)"
+
+#~ msgid "HTTP Listen Port"
+#~ msgstr "HTTP 監聽埠"
+
+#~ msgid "HTTPS Listen Port"
+#~ msgstr "HTTPS 監聽埠"
+
+#~ msgid "Index (index)"
+#~ msgstr "網站首頁 (index)"
+
+#~ msgid "Private Key Path (ssl_certificate_key)"
+#~ msgstr "私鑰路徑 (ssl_certificate_key)"
+
+#~ msgid "Root Directory (root)"
+#~ msgstr "網站根目錄 (root)"
+
+#~ msgid "Server Names (server_name)"
+#~ msgstr "網站域名 (server_name)"
+
+#~ msgid ""
+#~ "The certificate for the domain will be checked every hour, and will be "
+#~ "renewed if it has been more than 1 month since it was last issued.<br/>If "
+#~ "you do not have a certificate before, please click \"Getting Certificate "
+#~ "from Let's Encrypt\" first."
+#~ msgstr ""
+#~ "系統將會每小時檢測一次該域名證書,若距離上次簽發已超過1個月,則將自動續"
+#~ "簽。<br/>如果您之前沒有證書,請先點選「從 Let's Encrypt 獲取證書」。"
+
+#~ msgid "Do you want to change the template to support the TLS?"
+#~ msgstr "你想要改變模板以支援 TLS 嗎?"
+
+#~ msgid "Edit Configuration File"
+#~ msgstr "編輯配置檔案"
+
+#~ msgid "Getting Certificate from Let's Encrypt"
+#~ msgstr "從 Let's Encrypt 獲取證書"
+
+#~ msgid "Skip"
+#~ msgstr "跳過"
+
+#~ msgid ""
+#~ "The following values will only take effect if you have the corresponding "
+#~ "fields in your configuration file. The configuration filename cannot be "
+#~ "changed after it has been created."
+#~ msgstr ""
+#~ "只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可"
+#~ "修改。"
+
+#~ msgid "This feature is not available in demo."
+#~ msgstr "此功能在演示中不可用。"
+
+#~ msgid "This operation will lose the custom configuration."
+#~ msgstr "該操作將會丟失自定義配置。"
+
+#~ msgid ""
+#~ "Add site here first, then you can configure TLS on the domain edit page."
+#~ msgstr "在這裡新增站點,完成後可進入編輯頁面配置 TLS。"
+
+#~ msgid "Server Status"
+#~ msgstr "伺服器狀態"
+
+#~ msgid "Used: %{u}, Cached: %{c}, Free: %{f}, Physical Memory: %{p}"
+#~ msgstr "已使用: %{u}, 快取: %{c}, 空閒: %{f}, 物理記憶體: %{p}"
+
+#~ msgid "Used: %{used} / Total: %{total}"
+#~ msgstr "已使用: %{used} / 總共: %{total}"
+
+#~ msgid "CPU"
+#~ msgstr "CPU"

+ 229 - 0
frontend-next/src/layouts/BaseLayout.vue

@@ -0,0 +1,229 @@
+<template>
+    <a-config-provider :locale="lang">
+        <a-layout style="min-height: 100%;">
+            <a-drawer
+                v-show="clientWidth<512"
+                :closable="false"
+                :visible="collapsed"
+                placement="left"
+                @close="collapsed=false"
+            >
+                <side-bar/>
+            </a-drawer>
+
+            <a-layout-sider
+                v-show="clientWidth>=512"
+                v-model="collapsed"
+                :collapsible="true"
+                :style="{zIndex: 11}"
+                theme="light"
+                class="layout-sider"
+            >
+                <side-bar/>
+            </a-layout-sider>
+
+            <a-layout>
+                <a-layout-header :style="{position: 'fixed', zIndex: 10, width:'100%'}">
+                    <header-layout @clickUnFold="collapsed=true"/>
+                </a-layout-header>
+
+                <a-layout-content>
+                    <page-header/>
+                    <div class="router-view">
+                        <router-view/>
+                    </div>
+                </a-layout-content>
+
+                <a-layout-footer>
+                    <footer-layout/>
+                </a-layout-footer>
+            </a-layout>
+
+        </a-layout>
+    </a-config-provider>
+</template>
+
+<script>
+import HeaderLayout from './HeaderLayout.vue'
+import SideBar from './SideBar.vue'
+import FooterLayout from './FooterLayout.vue'
+import PageHeader from '@/components/PageHeader/PageHeader.vue'
+import zh_CN from 'ant-design-vue/es/locale/zh_CN'
+import zh_TW from 'ant-design-vue/es/locale/zh_TW'
+import en_US from 'ant-design-vue/es/locale/en_US'
+
+export default {
+    name: 'BaseLayout',
+    data() {
+        return {
+            collapsed: this.collapse(),
+            clientWidth: document.body.clientWidth,
+        }
+    },
+    mounted() {
+        window.onresize = () => {
+            this.collapsed = this.collapse()
+            this.clientWidth = this.getClientWidth()
+        }
+    },
+    components: {
+        SideBar,
+        PageHeader,
+        HeaderLayout,
+        FooterLayout
+    },
+    methods: {
+        getClientWidth() {
+            return document.body.clientWidth
+        },
+        collapse() {
+            return !(this.getClientWidth() > 768 || this.getClientWidth() < 512)
+        }
+    },
+    computed: {
+        lang: {
+            get() {
+                switch (this.$language.current) {
+                    case 'zh_CN':
+                        return zh_CN
+                    case 'zh_TW':
+                        return zh_TW
+                    default:
+                        return en_US
+                }
+            }
+        }
+    }
+}
+</script>
+<style lang="less">
+.layout-sider .sidebar {
+    //position: fixed;
+    //width: 200px;
+
+    ul.ant-menu-inline.ant-menu-root {
+        height: calc(100vh - 120px);
+        overflow-y: auto;
+        overflow-x: hidden;
+
+        .ant-menu-item {
+            width: unset;
+        }
+    }
+
+    ul.ant-menu-inline-collapsed {
+        height: calc(100vh - 200px);
+        overflow-y: auto;
+        overflow-x: hidden;
+    }
+}
+</style>
+
+<style lang="less">
+@dark: ~"(prefers-color-scheme: dark)";
+
+body {
+    overflow: unset !important;
+}
+
+@media @dark {
+    h1, h2, h3, h4, h5, h6, p {
+        color: #fafafa !important;
+    }
+
+}
+
+.ant-layout-header {
+    background-color: #fff;
+    @media @dark {
+        background-color: #1f1f1f !important;
+    }
+}
+
+.ant-card {
+    @media @dark {
+        background-color: #1f1f1f !important;
+    }
+}
+
+.ant-layout-sider {
+    background-color: #ffffff;
+    @media @dark {
+        background-color: rgb(20, 20, 20) !important;
+        .ant-layout-sider-trigger {
+            background-color: rgb(20, 20, 20) !important;
+        }
+
+        .ant-menu {
+            border-right: 0 !important;
+        }
+    }
+
+    &.ant-layout-sider-has-trigger {
+        padding-bottom: 0;
+    }
+
+    box-shadow: 2px 0 8px rgba(29, 35, 41, 0.05);
+}
+
+.ant-drawer-body {
+    .sidebar .logo {
+        box-shadow: 0 1px 0 0 #e8e8e8;
+    }
+
+    .ant-menu-inline .ant-menu-selected::after, .ant-menu-inline .ant-menu-item-selected::after {
+        border-right: 0 !important;
+    }
+}
+
+@media @dark {
+    .ant-checkbox-indeterminate {
+        .ant-checkbox-inner {
+            background-color: transparent !important;
+        }
+    }
+}
+
+.ant-layout-header {
+    padding: 0 !important;
+}
+
+.ant-table-small {
+    font-size: 13px;
+}
+
+.ant-card-bordered {
+
+}
+
+.header-notice-wrapper .ant-tabs-content {
+    max-height: 250px;
+}
+
+.header-notice-wrapper .ant-tabs-tabpane-active {
+    overflow-y: scroll;
+}
+
+.ant-layout-footer {
+    @media (max-width: 320px) {
+        padding: 10px;
+    }
+}
+
+.ant-layout-content {
+    margin: 64px 0;
+    min-height: auto;
+
+    .router-view {
+        padding: 20px;
+        @media (max-width: 512px) {
+            padding: 20px 0;
+        }
+        position: relative;
+    }
+}
+
+.ant-layout-footer {
+    text-align: center;
+}
+</style>

+ 13 - 0
frontend-next/src/layouts/BaseRouterView.vue

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

+ 22 - 0
frontend-next/src/layouts/FooterLayout.vue

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

+ 83 - 0
frontend-next/src/layouts/HeaderLayout.vue

@@ -0,0 +1,83 @@
+<script setup lang="ts">
+import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
+import gettext from '@/gettext'
+import {message} from "ant-design-vue"
+import auth from '@/api/auth'
+import {HomeOutlined, LogoutOutlined} from "@ant-design/icons-vue"
+
+const {$gettext} = gettext
+import {useRouter} from "vue-router"
+
+const router = useRouter()
+
+function logout() {
+    auth.logout().then(() => {
+        message.success($gettext('Logout successful'))
+        window.location.reload()
+    })
+}
+
+</script>
+
+<template>
+    <div class="header">
+        <div class="tool">
+            <a-icon type="menu-unfold" @click="$emit('clickUnFold')"/>
+        </div>
+        <div class="user-wrapper">
+            <set-language class="set_lang"/>
+
+            <a href="/">
+                <HomeOutlined/>
+            </a>
+
+            <a @click="logout" style="margin-left: 20px">
+                <LogoutOutlined/>
+            </a>
+        </div>
+    </div>
+</template>
+
+
+<style lang="less" scoped>
+.header {
+    height: 64px;
+    padding: 0 20px 0 0;
+    background: transparent;
+    box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
+    position: fixed;
+    width: 100%;
+
+    a {
+        color: #000000;
+    }
+}
+
+@media (prefers-color-scheme: dark) {
+    .header {
+        box-shadow: 1px 1px 0 0 #404040;
+
+        a {
+            color: #fafafa;
+        }
+    }
+}
+
+.tool {
+    position: fixed;
+    left: 20px;
+    @media (min-width: 512px) {
+        display: none;
+    }
+}
+
+.user-wrapper {
+    position: fixed;
+    right: 20px;
+}
+
+.set_lang {
+    display: inline;
+    margin-right: 25px;
+}
+</style>

+ 33 - 0
frontend-next/src/layouts/Loading.vue

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

+ 124 - 0
frontend-next/src/layouts/SideBar.vue

@@ -0,0 +1,124 @@
+<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()
+
+const route = useRoute()
+
+let openKeys = [openSub()]
+
+const selectedKey = ref()
+
+
+function openSub() {
+    let path = route.path
+    let lastSepIndex = path.lastIndexOf('/')
+    return path.substring(1, lastSepIndex)
+}
+
+function onOpenChange(_openKeys: Array<any>) {
+    const latestOpenKey = openKeys.find(key => openKeys.indexOf(key) === -1) || ''
+    if ((sidebars.value||[]).indexOf(latestOpenKey) === -1) {
+        openKeys = _openKeys
+    } else {
+        openKeys = latestOpenKey ? [latestOpenKey] : []
+    }
+}
+
+watch(route, ()=>{
+    const selectedKey = [route.name]
+    const sub = openSub()
+    const p = openKeys.indexOf(sub)
+    if (p === -1) openKeys.push(sub)
+})
+
+const sidebars = computed(()=>{
+    return routes[0]['children']
+})
+
+interface meta {
+    icon: any
+    hiddenInSidebar: boolean
+}
+
+interface sidebar {
+    path: string
+    name: string
+    meta: meta,
+    children: sidebar[]
+}
+
+const visible = computed(()=>{
+
+    const res: sidebar[] = [];
+
+    (sidebars.value||[]).forEach((s)=> {
+        if (s.meta && s.meta.hiddenInSidebar) {
+            return
+        }
+        const t: sidebar = {
+            path: s.path,
+            name: s.name,
+            meta: s.meta as meta,
+            children: []
+        };
+
+        (s.children||[]).forEach(c => {
+            if (c.meta && c.meta.hiddenInSidebar) {
+                return
+            }
+            t.children.push((c as sidebar))
+        })
+        res.push(t)
+    })
+
+
+   return res
+})
+
+</script>
+
+<template>
+    <div class="sidebar">
+        <logo/>
+        <a-menu
+            :openKeys="openKeys"
+            mode="inline"
+            @openChange="onOpenChange"
+            v-model="selectedKey"
+        >
+            <template v-for="sidebar in visible">
+                <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren === true" :key="sidebar.name"
+                             @click="$router.push('/'+sidebar.path).catch(() => {})">
+                    <component :is="sidebar.meta.icon" />
+                    <span>{{ $gettext(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>
+                    </template>
+                    <a-menu-item v-for="child in sidebar.children" :key="child.name">
+                        <router-link :to="'/'+sidebar.path+'/'+child.path">
+                            {{ $gettext(child.name) }}
+                        </router-link>
+                    </a-menu-item>
+                </a-sub-menu>
+            </template>
+        </a-menu>
+    </div>
+</template>
+
+<style lang="less">
+.ant-layout-sider-collapsed .logo {
+    overflow: hidden;
+}
+
+.ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left {
+    border-right: unset;
+}
+</style>

+ 52 - 0
frontend-next/src/lib/http/index.ts

@@ -0,0 +1,52 @@
+import axios from 'axios'
+import {useUserStore} from "@/pinia/user"
+import {storeToRefs} from "pinia";
+
+const user = useUserStore()
+
+const {token} = storeToRefs(user)
+
+/* 创建 axios 实例 */
+let http = axios.create({
+    baseURL: import.meta.env.VITE_API_ROOT,
+    timeout: 50000,
+    headers: {'Content-Type': 'application/json'},
+    transformRequest: [function (data, headers) {
+        if (!(headers) || headers['Content-Type'] === 'multipart/form-data;charset=UTF-8') {
+            return data
+        } else {
+            headers['Content-Type'] = 'application/json'
+        }
+        return JSON.stringify(data)
+    }],
+})
+
+/* http request 拦截器 */
+http.interceptors.request.use(
+    config => {
+        if (token) {
+            (config.headers || {}).Authorization = token.value
+        }
+        return config
+    },
+    err => {
+        return Promise.reject(err)
+    }
+)
+
+/* response 拦截器 */
+http.interceptors.response.use(
+    response =>{
+        return Promise.resolve(response)
+    },
+    async error => {
+        switch (error.response.status) {
+            case 401:
+            case 403:
+                break
+        }
+        return Promise.reject(error.response.data)
+    }
+)
+
+export default http

+ 24 - 0
frontend-next/src/main.ts

@@ -0,0 +1,24 @@
+import {createApp} from 'vue'
+import {createPinia} from "pinia"
+import gettext from "./gettext"
+import App from './App.vue'
+import router from "./routes"
+import 'ant-design-vue/dist/antd.less'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import {useSettingsStore} from "@/pinia/settings"
+
+
+const pinia = createPinia()
+
+const app = createApp(App)
+
+pinia.use(piniaPluginPersistedstate)
+app.use(pinia)
+app.use(gettext)
+// after pinia created
+const settings = useSettingsStore()
+gettext.current = settings.language || 'en'
+
+app.use(router).mount('#app')
+
+export default app

+ 16 - 0
frontend-next/src/pinia/settings.ts

@@ -0,0 +1,16 @@
+import { defineStore } from "pinia"
+
+export const useSettingsStore = defineStore('settings', {
+    state: () => ({
+        language: '',
+    }),
+    getters: {
+
+    },
+    actions: {
+        set_language(lang:string) {
+            this.language = lang
+        },
+    },
+    persist: true
+})

+ 20 - 0
frontend-next/src/pinia/user.ts

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

+ 163 - 0
frontend-next/src/routes/index.ts

@@ -0,0 +1,163 @@
+import {createRouter, createWebHistory} from "vue-router"
+import gettext from "../gettext"
+
+const {$gettext} = gettext
+
+import {useUserStore} from "@/pinia/user"
+import {
+    HomeOutlined,
+    UserOutlined,
+    CloudOutlined,
+    FileOutlined,
+    CodeOutlined,
+    InfoCircleOutlined
+
+} from "@ant-design/icons-vue"
+
+export const routes = [
+    {
+        path: '/',
+        name: $gettext('Home'),
+        component: () => import('@/layouts/BaseLayout.vue'),
+        redirect: '/dashboard',
+        children: [
+            {
+                path: 'dashboard',
+                component: () => import('@/views/dashboard/DashBoard.vue'),
+                name: $gettext('Dashboard'),
+                meta: {
+                    hiddenHeaderContent: true,
+                    icon: HomeOutlined
+                }
+            },
+            {
+                path: 'user',
+                name: $gettext('Manage Users'),
+                // component: () => import('@/views/user/User.vue'),
+                meta: {
+                    icon: UserOutlined
+                },
+            },
+            {
+                path: 'domain',
+                name: $gettext('Manage Sites'),
+                component: () => import('@/layouts/BaseRouterView.vue'),
+                meta: {
+                    icon: CloudOutlined
+                },
+                redirect: '/domain/list',
+                children: [{
+                    path: 'list',
+                    name: $gettext('Sites List'),
+                    // component: () => import('@/views/domain/DomainList.vue'),
+                }, {
+                    path: 'add',
+                    name: $gettext('Add Site'),
+                    // component: () => import('@/views/domain/DomainAdd.vue'),
+                }, {
+                    path: ':name',
+                    name: $gettext('Edit Site'),
+                    // component: () => import('@/views/domain/DomainEdit.vue'),
+                    meta: {
+                        hiddenInSidebar: true
+                    }
+                },]
+            },
+            {
+                path: 'config',
+                name: $gettext('Manage Configs'),
+                // component: () => import('@/views/config/Config.vue'),
+                meta: {
+                    icon: FileOutlined,
+                    hideChildren: true
+                }
+            },
+            {
+                path: 'config/:name',
+                name: $gettext('Edit Configuration'),
+                // component: () => import('@/views/config/ConfigEdit.vue'),
+                meta: {
+                    hiddenInSidebar: true
+                },
+            },
+            {
+                path: 'terminal',
+                name: $gettext('Terminal'),
+                component: () => import('@/views/pty/Terminal.vue'),
+                meta: {
+                    icon: CodeOutlined
+                }
+            },
+            {
+                path: 'about',
+                name: $gettext('About'),
+                component: () => import('@/views/other/About.vue'),
+                meta: {
+                    icon: InfoCircleOutlined
+                }
+            },
+        ]
+    },
+    // {
+    //     path: '/install',
+    //     name: $gettext('Install'),
+    //     component: () => import('@/views/other/Install.vue'),
+    //     meta: {noAuth: true}
+    // },
+    {
+        path: '/login',
+        name: $gettext('Login'),
+        component: () => import('@/views/other/Login.vue'),
+        meta: {noAuth: true}
+    },
+    {
+        path: '/404',
+        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'),
+        redirect: '/404',
+        meta: {noAuth: true}
+    }
+]
+
+const router = createRouter({
+    history: createWebHistory(),
+    // @ts-ignore
+    routes: routes,
+})
+
+router.beforeEach((to, from, next) => {
+    document.title = to.name as string + ' | Nginx UI'
+
+    if (import.meta.env.MODE === 'production') {
+        // axios.get('/version.json?' + Date.now()).then(r => {
+        //     if (!(process.env.VUE_APP_VERSION === r.data.version
+        //         && Number(process.env.VUE_APP_BUILD_ID) === r.data.build_id)) {
+        //         Vue.prototype.$info({
+        //             title: $gettext('System message'),
+        //             content: $gettext('Detected version update, this page will refresh.'),
+        //             onOk() {
+        //                 location.reload()
+        //             },
+        //             okText: $gettext('OK')
+        //         })
+        //     }
+        // })
+    }
+
+    const user = useUserStore()
+    const {is_login} = user
+
+    if (to.meta.noAuth || is_login) {
+        next()
+    } else {
+        next({path: '/login', query: {next: to.fullPath}})
+    }
+
+})
+
+export default router

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

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

+ 56 - 0
frontend-next/src/views/config/Config.vue

@@ -0,0 +1,56 @@
+<template>
+    <a-card :title="$gettext('Configurations')">
+        <std-table
+            :api="api"
+            :columns="columns"
+            :deletable="false"
+            :disable_search="true"
+            data_key="configs"
+            row-key="name"
+            @clickEdit="r => {
+                $router.push({
+                    path: '/config/' + r
+                })
+            }"
+        />
+    </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>

+ 78 - 0
frontend-next/src/views/config/ConfigEdit.vue

@@ -0,0 +1,78 @@
+<template>
+    <a-card :title="$gettext('Edit Configuration')">
+        <vue-itextarea v-model="configText"/>
+        <footer-tool-bar>
+            <a-space>
+                <a-button @click="$router.go(-1)">
+                    <translate>Cancel</translate>
+                </a-button>
+                <a-button type="primary" @click="save">
+                    <translate>Save</translate>
+                </a-button>
+            </a-space>
+        </footer-tool-bar>
+    </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;
+    @media (max-width: 512px) {
+        margin: 10px 0;
+    }
+}
+</style>

+ 327 - 0
frontend-next/src/views/dashboard/DashBoard.vue

@@ -0,0 +1,327 @@
+<template>
+    <div>
+        <a-row :gutter="[16,16]" class="first-row">
+            <a-col :xl="7" :lg="24" :md="24">
+                <a-card :title="$gettext('Server Info')">
+                    <p>
+                        <translate>Uptime:</translate>
+                        {{ uptime }}
+                    </p>
+                    <p>
+                        <translate>Load Averages:</translate>
+                        1min:{{ loadavg?.load1?.toFixed(2) }} |
+                        5min:{{ loadavg?.load5?.toFixed(2) }} |
+                        15min:{{ loadavg?.load15?.toFixed(2) }}
+                    </p>
+                    <p>
+                        <translate>OS:</translate>
+                        {{ host.platform }} ({{ host.platformVersion }}
+                        {{ host.os }} {{ host.kernelVersion }}
+                        {{ host.kernelArch }})
+                    </p>
+                    <p v-if="cpu_info">
+                        <translate>CPU:</translate>
+                        {{ cpu_info[0]?.modelName }} * {{ cpu_info.length }}
+                    </p>
+                </a-card>
+            </a-col>
+            <a-col :xl="10" :lg="16" :md="24" class="chart_dashboard">
+                <a-card :title="$gettext('Memory and Storage')">
+                    <a-row :gutter="[0,16]">
+                        <a-col :xs="24" :sm="24" :md="8">
+                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory_pressure]"
+                                              :centerText="memory_used" :bottom-text="memory_total" colors="#36a3eb"/>
+                        </a-col>
+                        <a-col :xs="24" :sm="12" :md="8">
+                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory_swap_percent]"
+                                              :centerText="memory_swap_used"
+                                              :bottom-text="memory_swap_total" colors="#ff6385"/>
+                        </a-col>
+                        <a-col :xs="24" :sm="12" :md="8">
+                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk_percentage]"
+                                              :centerText="disk_used" :bottom-text="disk_total" colors="#87d068"/>
+                        </a-col>
+                    </a-row>
+                </a-card>
+            </a-col>
+            <a-col :xl="7" :lg="8" :sm="24" class="chart_dashboard">
+                <a-card :title="$gettext('Network Statistics')">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.last_recv)"
+                                         :title="$gettext('Network Total Receive')"/>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.last_sent)"
+                                         :title="$gettext('Network Total Send')"/>
+                        </a-col>
+                    </a-row>
+                </a-card>
+            </a-col>
+        </a-row>
+        <a-row class="row-two" :gutter="[16,32]">
+            <a-col :xl="8" :lg="24" :md="24" :sm="24">
+                <a-card :title="$gettext('CPU Status')">
+                    <a-statistic :value="cpu" title="CPU">
+                        <template v-slot:suffix>
+                            <span>%</span>
+                        </template>
+                    </a-statistic>
+                    <c-p-u-chart :series="cpu_analytic_series"/>
+                </a-card>
+            </a-col>
+            <a-col :xl="8" :lg="12" :md="24" :sm="24">
+                <a-card :title="$gettext('Network')">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.recv)"
+                                         :title="$gettext('Receive')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.sent)" :title="$gettext('Send')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                    </a-row>
+                    <net-chart :series="net_analytic"/>
+                </a-card>
+            </a-col>
+            <a-col :xl="8" :lg="12" :md="24" :sm="24">
+                <a-card :title="$gettext('Disk IO')">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="diskIO.writes"
+                                         :title="$gettext('Writes')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="diskIO.reads" :title="$gettext('Reads')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                    </a-row>
+                    <disk-chart :series="diskIO_analytic"/>
+                </a-card>
+            </a-col>
+        </a-row>
+
+    </div>
+</template>
+
+<script>
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import CPUChart from '@/components/Chart/CPUChart.vue'
+import NetChart from '@/components/Chart/NetChart.vue'
+import $gettext from '@/lib/translate/gettext.vue'
+import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
+import DiskChart from '@/components/Chart/DiskChart.vue'
+
+export default {
+    name: 'DashBoard',
+    components: {
+        DiskChart,
+        RadialBarChart,
+        NetChart,
+        CPUChart,
+    },
+    data() {
+        return {
+            websocket: null,
+            loading: true,
+            stat: {},
+            memory_pressure: 0,
+            memory_used: '',
+            memory_cached: '',
+            memory_free: '',
+            memory_total: '',
+            cpu_analytic_series: [{
+                name: 'CPU User',
+                data: []
+            }, {
+                name: 'CPU Total',
+                data: []
+            }],
+            cpu: 0,
+            memory_swap_used: '',
+            memory_swap_total: '',
+            memory_swap_percent: 0,
+            disk_percentage: 0,
+            disk_total: '',
+            disk_used: '',
+            net: {
+                recv: 0,
+                sent: 0,
+                last_recv: 0,
+                last_sent: 0,
+            },
+            diskIO: {
+                writes: 0,
+                reads: 0,
+            },
+            net_analytic: [{
+                name: $gettext('Receive'),
+                data: []
+            }, {
+                name: $gettext('Send'),
+                data: []
+            }],
+            diskIO_analytic: [{
+                name: $gettext('Writes'),
+                data: []
+            }, {
+                name: $gettext('Reads'),
+                data: []
+            }],
+            uptime: '',
+            loadavg: {},
+            cpu_info: [],
+            host: {}
+        }
+    },
+    created() {
+        this.websocket = new ReconnectingWebSocket(this.getWebSocketRoot() + '/analytic?token='
+            + btoa(this.$store.state.user.token))
+        this.websocket.onmessage = this.wsOnMessage
+        this.websocket.onopen = this.wsOpen
+        this.$api.analytic.init().then(r => {
+            this.cpu_info = r.cpu.info
+            this.net.last_recv = r.network.init.bytesRecv
+            this.net.last_sent = r.network.init.bytesSent
+            this.host = r.host
+            r.cpu.user.forEach(u => {
+                this.cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
+            })
+            r.cpu.total.forEach(u => {
+                this.cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
+            })
+            r.network.bytesRecv.forEach(u => {
+                this.net_analytic[0].data.push([u.x, u.y.toFixed(2)])
+            })
+            r.network.bytesSent.forEach(u => {
+                this.net_analytic[1].data.push([u.x, u.y.toFixed(2)])
+            })
+            this.diskIO_analytic[0].data = this.diskIO_analytic[0].data.concat(r.diskIO.writes)
+            this.diskIO_analytic[1].data = this.diskIO_analytic[1].data.concat(r.diskIO.reads)
+        })
+    },
+    destroyed() {
+        this.websocket.close()
+    },
+    methods: {
+        wsOpen() {
+            this.websocket.send('ping')
+        },
+        wsOnMessage(m) {
+            const r = JSON.parse(m.data)
+            // console.log(r)
+            this.cpu = r.cpu_system + r.cpu_user
+            this.cpu = this.cpu.toFixed(2)
+            const time = new Date().getTime()
+
+            this.cpu_analytic_series[0].data.push([time, r.cpu_user.toFixed(2)])
+            this.cpu_analytic_series[1].data.push([time, this.cpu])
+
+            if (this.cpu_analytic_series[0].data.length > 100) {
+                this.cpu_analytic_series[0].data.shift()
+                this.cpu_analytic_series[1].data.shift()
+            }
+
+            // mem
+            this.memory_pressure = r.memory_pressure
+            this.memory_used = r.memory_used
+            this.memory_cached = r.memory_cached
+            this.memory_free = r.memory_free
+            this.memory_total = r.memory_total
+            this.memory_swap_percent = r.memory_swap_percent
+            this.memory_swap_used = r.memory_swap_used
+            this.memory_swap_total = r.memory_swap_total
+
+            // disk
+            this.disk_percentage = r.disk_percentage
+            this.disk_used = r.disk_used
+            this.disk_total = r.disk_total
+
+            let uptime = Math.floor(r.uptime)
+            let uptime_days = Math.floor(uptime / 86400)
+            uptime -= uptime_days * 86400
+            let uptime_hours = Math.floor(uptime / 3600)
+            uptime -= uptime_hours * 3600
+            this.uptime = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(uptime / 60) + 'm'
+            this.loadavg = r.loadavg
+
+            // net
+            this.net.recv = r.network.bytesRecv - this.net.last_recv
+            this.net.sent = r.network.bytesSent - this.net.last_sent
+            this.net.last_recv = r.network.bytesRecv
+            this.net.last_sent = r.network.bytesSent
+
+            this.net_analytic[0].data.push([time, this.net.recv])
+            this.net_analytic[1].data.push([time, this.net.sent])
+
+            if (this.net_analytic[0].data.length > 100) {
+                this.net_analytic[0].data.shift()
+                this.net_analytic[1].data.shift()
+            }
+
+            // diskIO
+            this.diskIO.writes = r.diskIO.writes.y
+            this.diskIO.reads = r.diskIO.reads.y
+
+            this.diskIO_analytic[0].data.push(r.diskIO.writes)
+            this.diskIO_analytic[1].data.push(r.diskIO.reads)
+
+            if (this.diskIO_analytic[0].data.length > 100) {
+                this.diskIO_analytic[0].data.shift()
+                this.diskIO_analytic[1].data.shift()
+            }
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.first-row {
+    .ant-card {
+        min-height: 227px;
+    }
+}
+
+.ant-card {
+    .ant-statistic {
+        margin: 0 50px 10px 10px
+    }
+
+    .chart {
+        max-width: 800px;
+        max-height: 350px;
+    }
+
+    .chart_dashboard {
+        padding: 60px;
+
+        .description {
+            width: 120px;
+            text-align: center
+        }
+    }
+
+    @media (max-width: 512px) {
+        margin: 10px 0;
+        .chart_dashboard {
+            padding: 20px;
+        }
+    }
+}
+</style>
+

+ 159 - 0
frontend-next/src/views/domain/DomainAdd.vue

@@ -0,0 +1,159 @@
+<template>
+    <a-card :title="$gettext('Add Site')">
+        <div class="domain-add-container">
+            <a-steps :current="current_step" size="small">
+                <a-step :title="$gettext('Base information')"/>
+                <a-step :title="$gettext('Configure SSL')"/>
+                <a-step :title="$gettext('Finished')"/>
+            </a-steps>
+
+            <template v-if="current_step===0">
+                <a-form-item :label="$gettext('Configuration Name')">
+                    <a-input v-model="config.name"/>
+                </a-form-item>
+
+                <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
+
+                <location-editor :locations="ngx_config.servers[0].locations"/>
+
+                <a-alert
+                    v-if="!has_server_name"
+                    :message="$gettext('Warning')"
+                    type="warning"
+                    show-icon
+                >
+                    <template slot="description">
+                    <span v-translate>
+                        server_name parameter is required
+                    </span>
+                    </template>
+                </a-alert>
+                <br/>
+            </template>
+
+            <template v-else-if="current_step===1">
+
+                <ngx-config-editor
+                    ref="ngx_config"
+                    :ngx_config="ngx_config"
+                    v-model="auto_cert"
+                    :enabled="enabled"
+                />
+
+            </template>
+
+            <a-space v-if="current_step<2">
+                <a-button
+                    type="primary"
+                    @click="save"
+                    :disabled="!config.name||!has_server_name"
+                >
+                    <translate>Next</translate>
+                </a-button>
+            </a-space>
+
+            <a-result
+                v-else-if="current_step===2"
+                status="success"
+                :title="$gettext('Domain Config Created Successfully')"
+            >
+                <template #extra>
+                    <a-button type="primary" @click="goto_modify">
+                        <translate>Modify Config</translate>
+                    </a-button>
+                    <a-button @click="create_another">
+                        <translate>Create Another</translate>
+                    </a-button>
+                </template>
+            </a-result>
+
+        </div>
+    </a-card>
+</template>
+
+<script>
+import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
+import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
+import $gettext, {$interpolate} from '@/lib/translate/gettext'
+import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
+
+export default {
+    name: 'DomainAdd',
+    components: {NgxConfigEditor, LocationEditor, DirectiveEditor},
+    data() {
+        return {
+            config: {},
+            ngx_config: {
+                servers: [{}]
+            },
+            error: {},
+            current_step: 0,
+            enabled: true,
+            auto_cert: false
+        }
+    },
+    created() {
+        this.init()
+    },
+    methods: {
+        init() {
+            this.$api.domain.get_template().then(r => {
+                this.ngx_config = r.tokenized
+            })
+        },
+        save() {
+            this.$api.ngx.build_config(this.ngx_config).then(r => {
+                this.$api.domain.save(this.config.name, {content: r.content, enabled: true}).then(() => {
+                    this.$message.success($gettext('Saved successfully'))
+
+                    this.$api.domain.enable(this.config.name).then(() => {
+                        this.$message.success($gettext('Enabled successfully'))
+                        this.current_step++
+                    }).catch(r => {
+                        this.$message.error(r.message ?? $gettext('Enable failed'), 10)
+                    })
+
+                }).catch(r => {
+                    this.$message.error($interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}), 10)
+                })
+            })
+        },
+        goto_modify() {
+            this.$router.push('/domain/' + this.config.name)
+        },
+        create_another() {
+            this.current_step = 0
+            this.config = {}
+            this.ngx_config = {
+                servers: [{}]
+            }
+        },
+    },
+    computed: {
+        has_server_name() {
+            const servers = this.ngx_config.servers
+            for (const server_key in servers) {
+                for (const k in servers[server_key].directives) {
+                    const v = servers[server_key].directives[k]
+                    if (v.directive === 'server_name' && v.params.trim() !== '') {
+                        return true
+                    }
+                }
+            }
+
+            return false
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-steps {
+    padding: 10px 0 20px 0;
+}
+
+.domain-add-container {
+    max-width: 800px;
+    margin: 0 auto
+}
+</style>

+ 223 - 0
frontend-next/src/views/domain/DomainEdit.vue

@@ -0,0 +1,223 @@
+<template>
+    <div>
+        <a-card :bordered="false">
+            <template v-slot:title>
+                <span style="margin-right: 10px">{{ $gettextInterpolate($gettext('Edit %{n}'), {n: name}) }}</span>
+                <a-tag color="blue" v-if="enabled">
+                    {{ $gettext('Enabled') }}
+                </a-tag>
+                <a-tag color="orange" v-else>
+                    {{ $gettext('Disabled') }}
+                </a-tag>
+            </template>
+            <template v-slot:extra>
+                <a-switch size="small" v-model="advance_mode" @change="on_mode_change"/>
+                <template v-if="advance_mode">
+                    {{ $gettext('Advance Mode') }}
+                </template>
+                <template v-else>
+                    {{ $gettext('Basic Mode') }}
+                </template>
+            </template>
+
+            <transition name="slide-fade">
+                <div v-if="advance_mode" key="advance">
+                    <vue-itextarea v-model="configText"/>
+                </div>
+
+                <div class="domain-edit-container" key="basic" v-else>
+                    <a-form-item :label="$gettext('Enabled')">
+                        <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"
+                    />
+                </div>
+            </transition>
+
+        </a-card>
+
+        <footer-tool-bar>
+            <a-space>
+                <a-button @click="$router.go(-1)">
+                    <translate>Back</translate>
+                </a-button>
+                <a-button type="primary" @click="save" :loading="saving">
+                    <translate>Save</translate>
+                </a-button>
+            </a-space>
+        </footer-tool-bar>
+    </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>
+
+<style lang="less" scoped>
+.ant-card {
+    margin: 10px 0;
+    box-shadow: unset;
+}
+
+.domain-edit-container {
+    max-width: 800px;
+    margin: 0 auto;
+
+    /deep/ .ant-form-item-label > label::after {
+        content: none;
+    }
+}
+
+.slide-fade-enter-active {
+    transition: all .5s ease-in-out;
+}
+
+.slide-fade-leave-active {
+    transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0);
+}
+
+.slide-fade-enter, .slide-fade-leave-to
+    /* .slide-fade-leave-active for below version 2.1.8 */ {
+    transform: translateX(10px);
+    opacity: 0;
+}
+
+.location-block {
+
+}
+
+.directive-params-wrapper {
+    margin: 10px 0;
+}
+
+.tab-content {
+    padding: 10px;
+}
+</style>

+ 95 - 0
frontend-next/src/views/domain/DomainList.vue

@@ -0,0 +1,95 @@
+<template>
+    <a-card :title="$gettext('Manage Sites')">
+        <std-table
+            :api="api"
+            :columns="columns"
+            data_key="configs"
+            :disable_search="true"
+            row-key="name"
+            ref="table"
+            @clickEdit="r => this.$router.push({
+                path: '/domain/' + r
+            })"
+        >
+            <template #actions="{record}">
+                <a-divider type="vertical"/>
+                <a v-if="record.enabled" @click="disable(record.name)">
+                    {{ $gettext('Disabled') }}
+                </a>
+                <a v-else @click="enable(record.name)">
+                    {{ $gettext('Enabled') }}
+                </a>
+            </template>
+        </std-table>
+    </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>

+ 44 - 0
frontend-next/src/views/domain/cert/Cert.vue

@@ -0,0 +1,44 @@
+<template>
+    <div>
+        <cert-info ref="info" :domain="name" v-if="name"/>
+        <issue-cert
+            :current_server_directives="current_server_directives"
+            :directives-map="directivesMap"
+            v-model="auto_cert"
+            @callback="callback"
+        />
+    </div>
+</template>
+
+<script>
+import CertInfo from '@/views/domain/cert/CertInfo'
+import IssueCert from '@/views/domain/cert/IssueCert'
+
+export default {
+    name: 'Cert',
+    components: {IssueCert, CertInfo},
+    props: {
+        directivesMap: Object,
+        current_server_directives: Array,
+        auto_cert: Boolean
+    },
+    model: {
+        prop: 'auto_cert',
+        event: 'change_auto_cert'
+    },
+    methods: {
+        callback() {
+            this.$refs.info.get()
+        }
+    },
+    computed: {
+        name() {
+            return this.directivesMap['server_name'][0].params.trim()
+        }
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 74 - 0
frontend-next/src/views/domain/cert/CertInfo.vue

@@ -0,0 +1,74 @@
+<template>
+    <div class="cert-info" v-if="ok">
+        <h4 v-translate>Certificate Status</h4>
+        <p v-translate="{issuer: cert.issuer_name}">Intermediate Certification Authorities: %{issuer}</p>
+        <p v-translate="{name: cert.subject_name}">Subject Name: %{name}</p>
+        <p v-translate="{date: moment(cert.not_after).format('YYYY-MM-DD HH:mm:ss').toString()}">
+            Expiration Date: %{date}</p>
+        <p v-translate="{date: moment(cert.not_before).format('YYYY-MM-DD HH:mm:ss').toString()}">
+            Not Valid Before: %{date}</p>
+        <div class="status">
+            <template v-if="new Date().toISOString() < cert.not_before || new Date().toISOString() > cert.not_after">
+                <a-icon :style="{ color: 'red' }" type="close-circle"/>
+                <span v-translate>Certificate has expired</span>
+            </template>
+            <template v-else>
+                <a-icon :style="{ color: 'green' }" type="check-circle"/>
+                <span v-translate>Certificate is valid</span>
+            </template>
+        </div>
+    </div>
+</template>
+
+<script>
+import moment from 'moment'
+
+export default {
+    name: 'CertInfo',
+    data() {
+        return {
+            ok: false,
+            cert: {},
+            moment
+        }
+    },
+    props: {
+        domain: String
+    },
+    created() {
+        this.get()
+    },
+    watch: {
+        domain() {
+            this.get()
+        }
+    },
+    methods: {
+        get() {
+            this.$api.domain.cert_info(this.domain).then(r => {
+                this.cert = r
+                this.ok = true
+            }).catch(() => {
+                this.ok = false
+            })
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+h4 {
+    padding-bottom: 10px;
+}
+
+.cert-info {
+    padding-bottom: 10px;
+}
+
+.status {
+    span {
+        margin-left: 10px;
+    }
+}
+
+</style>

+ 169 - 0
frontend-next/src/views/domain/cert/IssueCert.vue

@@ -0,0 +1,169 @@
+<template>
+    <div>
+        <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
+            <a-switch
+                :loading="issuing_cert"
+                v-model="M_enabled"
+                @change="onchange"
+                :disabled="no_server_name||server_name_more_than_one"
+            />
+            <a-alert
+                v-if="no_server_name||server_name_more_than_one"
+                :message="$gettext('Warning')"
+                type="warning"
+                show-icon
+            >
+                <template slot="description">
+                    <span v-if="no_server_name" v-translate>
+                        server_name parameter is required
+                    </span>
+                    <span v-if="server_name_more_than_one" v-translate>
+                        server_name parameters more than one
+                    </span>
+                </template>
+            </a-alert>
+        </a-form-item>
+        <p v-translate>
+            Note: The server_name in the current configuration must be the domain name
+            you need to get the certificate.
+        </p>
+        <p v-if="enabled" v-translate>
+            The certificate for the domain will be checked every hour,
+            and will be renewed if it has been more than 1 month since it was last issued.
+        </p>
+        <p v-translate>
+            Make sure you have configured a reverse proxy for .well-known
+            directory to HTTPChallengePort (default: 9180) before getting the certificate.
+        </p>
+    </div>
+</template>
+
+<script>
+import {issue_cert} from '@/views/domain/methods'
+import $gettext, {$interpolate} from '@/lib/translate/gettext'
+
+export default {
+    name: 'IssueCert',
+    props: {
+        directivesMap: Object,
+        current_server_directives: Array,
+        enabled: Boolean
+    },
+    model: {
+        prop: 'enabled',
+        event: 'changeEnabled'
+    },
+    data() {
+        return {
+            issuing_cert: false,
+            M_enabled: this.enabled,
+        }
+    },
+    methods: {
+        onchange(r) {
+            this.$emit('changeEnabled', r)
+            this.change_auto_cert(r)
+            if (r) {
+                this.job()
+            }
+        },
+        job() {
+            this.issuing_cert = true
+
+            if (this.no_server_name) {
+                this.$message.error($gettext('server_name not found in directives'))
+                this.issuing_cert = false
+                return
+            }
+
+            if (this.server_name_more_than_one) {
+                this.$message.error($gettext('server_name parameters more than one'))
+                this.issuing_cert = false
+                return
+            }
+
+            const server_name = this.directivesMap['server_name'][0]
+
+            if (!this.directivesMap['ssl_certificate']) {
+                this.current_server_directives.splice(server_name.idx + 1, 0, {
+                    directive: 'ssl_certificate',
+                    params: ''
+                })
+            }
+
+            this.$nextTick(() => {
+                if (!this.directivesMap['ssl_certificate_key']) {
+                    const ssl_certificate = this.directivesMap['ssl_certificate'][0]
+                    this.current_server_directives.splice(ssl_certificate.idx + 1, 0, {
+                        directive: 'ssl_certificate_key',
+                        params: ''
+                    })
+                }
+            })
+
+            setTimeout(() => {
+                issue_cert(this.name, this.callback)
+            }, 100)
+        },
+        callback(ssl_certificate, ssl_certificate_key) {
+            this.$set(this.directivesMap['ssl_certificate'][0], 'params', ssl_certificate)
+            this.$set(this.directivesMap['ssl_certificate_key'][0], 'params', ssl_certificate_key)
+            this.issuing_cert = false
+            this.$emit('callback')
+        },
+        change_auto_cert(r) {
+            if (r) {
+                this.$api.domain.add_auto_cert(this.name).then(() => {
+                    this.$message.success($interpolate($gettext('Auto-renewal enabled for %{name}'), {name: this.name}))
+                }).catch(e => {
+                    this.$message.error(e.message ?? $interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: this.name}))
+                })
+            } else {
+                this.$api.domain.remove_auto_cert(this.name).then(() => {
+                    this.$message.success($interpolate($gettext('Auto-renewal disabled for %{name}'), {name: this.name}))
+                }).catch(e => {
+                    this.$message.error(e.message ?? $interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: this.name}))
+                })
+            }
+        },
+    },
+    watch: {
+        server_name_more_than_one() {
+            this.M_enabled = false
+            this.onchange(false)
+        },
+        no_server_name() {
+            this.M_enabled = false
+            this.onchange(false)
+        }
+    },
+    computed: {
+        is_demo() {
+            return this.$store.getters.env.demo === true
+        },
+        server_name_more_than_one() {
+            return this.directivesMap['server_name'] && (this.directivesMap['server_name'].length > 1 ||
+                this.directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
+        },
+        no_server_name() {
+            return !this.directivesMap['server_name']
+        },
+        name() {
+            return this.directivesMap['server_name'][0].params.trim()
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.switch-wrapper {
+    position: relative;
+
+    .text {
+        position: absolute;
+        top: 50%;
+        transform: translateY(-50%);
+        margin-left: 10px;
+    }
+}
+</style>

+ 37 - 0
frontend-next/src/views/domain/methods.js

@@ -0,0 +1,37 @@
+import $gettext from '@/lib/translate/gettext'
+import store from '@/lib/store'
+import Vue from 'vue'
+
+const issue_cert = (server_name, callback) => {
+    Vue.prototype.$message.info($gettext('Getting the certificate, please wait...'), 15)
+    const ws = new WebSocket(Vue.prototype.getWebSocketRoot() + '/cert/issue/' + server_name
+        + '?token=' + btoa(store.state.user.token))
+
+    ws.onopen = () => {
+        ws.send('go')
+    }
+
+    ws.onmessage = m => {
+        const r = JSON.parse(m.data)
+        switch (r.status) {
+            case 'success':
+                Vue.prototype.$message.success(r.message, 10)
+                break
+            case 'info':
+                Vue.prototype.$message.info(r.message, 10)
+                break
+            case 'error':
+                Vue.prototype.$message.error(r.message, 10)
+                break
+        }
+
+        if (r.status === 'success' && r.ssl_certificate !== undefined && r.ssl_certificate_key !== undefined) {
+            callback(r.ssl_certificate, r.ssl_certificate_key)
+        }
+    }
+    // setTimeout(() => {
+    //     callback('a', 'b')
+    // }, 10000)
+}
+
+export {issue_cert}

+ 78 - 0
frontend-next/src/views/domain/ngx_conf/LocationEditor.vue

@@ -0,0 +1,78 @@
+<template>
+    <a-form-item :label="$gettext('Locations')" :key="update">
+        <a-empty v-if="!locations"/>
+        <a-card v-for="(v,k) in locations" :key="k"
+                :title="$gettext('Location')" size="small">
+            <a-form-item :label="$gettext('Comments')" v-if="v.comments">
+                <p style="white-space: pre-wrap;">{{ v.comments }}</p>
+            </a-form-item>
+            <a-form-item :label="$gettext('Path')">
+                <a-input addon-before="location" v-model="v.path"/>
+            </a-form-item>
+            <a-form-item :label="$gettext('Content')">
+                <vue-itextarea v-model="v.content" :default-text-height="200"/>
+            </a-form-item>
+        </a-card>
+
+        <a-modal :title="$gettext('Add Location')" v-model="adding" @ok="save">
+            <a-form-item :label="$gettext('Comments')">
+                <a-textarea v-model="location.comments"></a-textarea>
+            </a-form-item>
+            <a-form-item :label="$gettext('Path')">
+                <a-input addon-before="location" v-model="location.path"/>
+            </a-form-item>
+            <a-form-item :label="$gettext('Content')">
+                <vue-itextarea v-model="location.content" :default-text-height="200"/>
+            </a-form-item>
+        </a-modal>
+
+        <div>
+            <a-button block @click="add">{{ $gettext('Add Location') }}</a-button>
+        </div>
+    </a-form-item>
+</template>
+
+<script>
+import VueItextarea from '@/components/VueItextarea/VueItextarea'
+
+export default {
+    name: 'LocationEditor',
+    components: {VueItextarea},
+    props: {
+        locations: Array
+    },
+    data() {
+        return {
+            adding: false,
+            location: {},
+            update: 0
+        }
+    },
+    methods: {
+        add() {
+            this.adding = true
+            this.location = {}
+        },
+        save() {
+            this.adding = false
+            if (this.locations) {
+                this.locations.push(this.location)
+            } else {
+                this.locations = [this.location]
+            }
+            this.update++
+        },
+        remove(index) {
+            this.update++
+            this.locations.splice(index, 1)
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.ant-card {
+    margin: 10px 0;
+    box-shadow: unset;
+}
+</style>

+ 181 - 0
frontend-next/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -0,0 +1,181 @@
+<template>
+    <div>
+        <a-form-item :label="$gettext('Enable TLS')" v-if="!support_ssl">
+            <a-switch @change="change_tls"/>
+        </a-form-item>
+
+        <a-tabs v-model="current_server_index">
+            <a-tab-pane :tab="'Server '+(k+1)" v-for="(v,k) in ngx_config.servers" :key="k">
+
+                <div class="tab-content">
+                    <template v-if="current_support_ssl&&enabled">
+                        <cert-info :domain="name" v-if="name"/>
+                        <issue-cert
+                            :current_server_directives="current_server_directives"
+                            :directives-map="directivesMap"
+                            v-model="auto_cert"
+                        />
+                        <cert-info :current_server_directives="current_server_directives"
+                                   :directives-map="directivesMap"
+                                   v-model="auto_cert"/>
+                    </template>
+
+                    <a-form-item :label="$gettext('Comments')" v-if="v.comments">
+                        <p style="white-space: pre-wrap;">{{ v.comments }}</p>
+                    </a-form-item>
+
+                    <directive-editor :ngx_directives="v.directives" :key="update"/>
+
+                    <location-editor :locations="v.locations"/>
+                </div>
+
+            </a-tab-pane>
+        </a-tabs>
+    </div>
+
+</template>
+
+<script>
+import CertInfo from '@/views/domain/cert/CertInfo'
+import IssueCert from '@/views/domain/cert/IssueCert'
+import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor'
+import LocationEditor from '@/views/domain/ngx_conf/LocationEditor'
+
+export default {
+    name: 'NgxConfigEditor',
+    components: {LocationEditor, DirectiveEditor, IssueCert, CertInfo},
+    props: {
+        ngx_config: Object,
+        auto_cert: Boolean,
+        enabled: Boolean
+    },
+    data() {
+        return {
+            current_server_index: 0,
+            update: 0,
+            name: this.$route.params?.name?.toString() ?? '',
+            init_ssl_status: false
+        }
+    },
+    model: {
+        prop: 'auto_cert',
+        event: 'change_auto_cert'
+    },
+    methods: {
+        update_cert_info() {
+            if (this.name && this.$refs['cert-info' + this.current_server_index]) {
+                this.$refs['cert-info' + this.current_server_index].get()
+            }
+        },
+        change_tls(r) {
+            if (r) {
+                // deep copy servers[0] to servers[1]
+                const server = JSON.parse(JSON.stringify(this.ngx_config.servers[0]))
+
+                this.ngx_config.servers.push(server)
+
+                this.current_server_index = 1
+
+                const servers = this.ngx_config.servers
+
+                let i = 0
+                while (i < servers[1].directives.length) {
+                    const v = servers[1].directives[i]
+                    if (v.directive === 'listen') {
+                        servers[1].directives.splice(i, 1)
+                    } else {
+                        i++
+                    }
+                }
+
+                servers[1].directives.splice(0, 0, {
+                    directive: 'listen',
+                    params: '443 ssl http2'
+                }, {
+                    directive: 'listen',
+                    params: '[::]:443 ssl http2'
+                })
+
+                const directivesMap = this.directivesMap
+
+                const server_name = directivesMap['server_name'][0]
+
+                if (!directivesMap['ssl_certificate']) {
+                    servers[1].directives.splice(server_name.idx + 1, 0, {
+                        directive: 'ssl_certificate',
+                        params: ''
+                    })
+                }
+
+                setTimeout(() => {
+                    if (!directivesMap['ssl_certificate_key']) {
+                        servers[1].directives.splice(server_name.idx + 2, 0, {
+                            directive: 'ssl_certificate_key',
+                            params: ''
+                        })
+                    }
+                }, 100)
+
+            } else {
+                // remove servers[1]
+                this.current_server_index = 0
+                if (this.ngx_config.servers.length === 2) {
+                    this.ngx_config.servers.splice(1, 1)
+                }
+            }
+        },
+    },
+    computed: {
+        directivesMap: {
+            get() {
+                const map = {}
+
+                this.current_server_directives.forEach((v, k) => {
+                    v.idx = k
+                    if (map[v.directive]) {
+                        map[v.directive].push(v)
+                    } else {
+                        map[v.directive] = [v]
+                    }
+                })
+
+                return map
+            }
+        },
+        current_server_directives: {
+            get() {
+                return this.ngx_config.servers[this.current_server_index].directives
+            }
+        },
+        support_ssl() {
+            const servers = this.ngx_config.servers
+            for (const server_key in servers) {
+                for (const k in servers[server_key].directives) {
+                    const v = servers[server_key].directives[k]
+                    if (v.directive === 'listen' && v.params.indexOf('ssl') > 0) {
+                        return true
+                    }
+                }
+            }
+            return false
+        },
+        current_support_ssl: {
+            get() {
+                if (this.directivesMap.listen) {
+                    for (const v of this.directivesMap.listen) {
+                        if (v?.params.indexOf('ssl') > 0) {
+                            return true
+                        }
+                    }
+                }
+
+                return false
+            }
+        },
+    }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 74 - 0
frontend-next/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -0,0 +1,74 @@
+<template>
+    <div>
+        <div class="add-directive-temp" v-if="adding">
+            <a-select v-model="mode" default-value="default" style="min-width: 150px">
+                <a-select-option value="default">
+                    {{ $gettext('Single Directive') }}
+                </a-select-option>
+                <a-select-option value="if">
+                    if
+                </a-select-option>
+            </a-select>
+            <vue-itextarea v-if="mode===If" :default-text-height="100" v-model="directive.params"/>
+            <a-input-group compact v-else>
+                <a-input style="width: 30%" :placeholder="$gettext('Directive')" v-model="directive.directive"/>
+                <a-input style="width: 70%" :placeholder="$gettext('Params')" v-model="directive.params">
+                    <a-icon slot="suffix" type="close" style="color: rgba(0,0,0,.45);font-size: 10px;"
+                            @click="adding=false"/>
+                </a-input>
+            </a-input-group>
+        </div>
+        <a-button block v-if="!adding" @click="add">{{ $gettext('Add Directive Below') }}</a-button>
+        <a-button type="primary" v-else block @click="save"
+                  :disabled="!directive.directive&&!directive.params">{{ $gettext('Save Directive') }}
+        </a-button>
+    </div>
+</template>
+
+<script>
+import {If} from '@/views/domain/ngx_conf/ngx_constant'
+import VueItextarea from '@/components/VueItextarea/VueItextarea'
+
+export default {
+    name: 'DirectiveAdd',
+    components: {
+        VueItextarea
+    },
+    props: {
+        ngx_directives: Array,
+        idx: Number,
+    },
+    data() {
+        return {
+            adding: false,
+            directive: {},
+            mode: 'default',
+            If
+        }
+    },
+    methods: {
+        add() {
+            this.adding = true
+            this.directive = {}
+        },
+        save() {
+            this.adding = false
+            if (this.mode === If) {
+                this.directive.directive = If
+            }
+
+            if (this.idx) {
+                this.ngx_directives.splice(this.idx + 1, 0, this.directive)
+            } else {
+                this.ngx_directives.push(this.directive)
+            }
+
+            this.$emit('save', this.idx)
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+
+</style>

+ 92 - 0
frontend-next/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -0,0 +1,92 @@
+<template>
+    <a-form-item :label="$gettext('Directives')">
+        <div v-for="(directive,k) in ngx_directives" :key="k" @click="current_idx=k">
+            <vue-itextarea v-if="directive.directive === If" v-model="directive.params" :default-text-height="100"/>
+            <a-input :addon-before="directive.directive" v-model="directive.params" @click="current_idx=k" v-else>
+                <a-popconfirm slot="suffix" @confirm="remove(k)"
+                              :title="$gettext('Are you sure you want to remove this directive?')"
+                              :ok-text="$gettext('Yes')"
+                              :cancel-text="$gettext('No')">
+                    <a-icon type="close"
+                            style="color: rgba(0,0,0,.45);font-size: 10px;"
+                    />
+                </a-popconfirm>
+            </a-input>
+            <transition name="slide">
+                <div v-if="current_idx===k" class="extra">
+                    <div class="extra-content">
+                        <a-form-item :label="$gettext('Comments')">
+                            <a-textarea v-model="directive.comments"/>
+                        </a-form-item>
+                        <directive-add :ngx_directives="ngx_directives" :idx="k" @save="onSave(k)"/>
+                    </div>
+                </div>
+            </transition>
+        </div>
+        <directive-add :ngx_directives="ngx_directives"/>
+    </a-form-item>
+</template>
+
+<script>
+import VueItextarea from '@/components/VueItextarea/VueItextarea'
+import {If} from '../ngx_constant'
+import DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
+
+export default {
+    name: 'DirectiveEditor',
+    props: {
+        ngx_directives: Array
+    },
+    components: {
+        DirectiveAdd,
+        VueItextarea
+    },
+    data() {
+        return {
+            adding: false,
+            directive: {},
+            If,
+            current_idx: -1,
+        }
+    },
+    methods: {
+        add() {
+            this.adding = true
+            this.directive = {}
+        },
+        save() {
+            this.adding = false
+            this.ngx_directives.push(this.directive)
+        },
+        remove(index) {
+            this.ngx_directives.splice(index, 1)
+        },
+        onSave(idx) {
+            const that = this
+            setTimeout(() => {
+                that.current_idx = idx + 1
+            }, 50)
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.extra {
+    background-color: #fafafa;
+    padding: 10px 20px 20px;
+    margin-bottom: 10px;
+}
+
+.slide-enter-active, .slide-leave-active {
+    transition: max-height .5s ease;
+}
+
+.slide-enter, .slide-leave-to {
+    max-height: 0;
+}
+
+.slide-enter-to, .slide-leave {
+    max-height: 600px;
+}
+</style>

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

@@ -0,0 +1 @@
+export const If = "if"

+ 48 - 0
frontend-next/src/views/other/About.vue

@@ -0,0 +1,48 @@
+<script setup lang="ts">
+import gettext from '@/gettext'
+const {$gettext} = gettext
+import logo from '@/assets/img/logo.png'
+
+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')
+const api_root = import.meta.env.VITE_API_ROOT
+</script>
+
+<template>
+    <a-card style="text-align: center" :bordered="false">
+        <div class="logo">
+            <img :src="logo" alt="logo"/>
+        </div>
+        <h2>Nginx UI</h2>
+        <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>
+        <h3 v-translate>Build with</h3>
+        <p>❤️</p>
+        <p>Go</p>
+        <p>Gin</p>
+        <p>Vue3 + Vite + TypeScript</p>
+        <p>Websocket</p>
+        <h3 v-translate translate-context="Project">License</h3>
+        <p>GNU General Public License v3.0</p>
+        <p>Copyright © 2020 - {{ this_year }} Nginx UI </p>
+    </a-card>
+</template>
+
+<style lang="less" scoped>
+.logo {
+    img {
+        max-width: 120px
+    }
+}
+
+.egg {
+    padding: 10px 0;
+}
+
+.ant-btn {
+    margin: 10px 10px 0 0;
+}
+</style>

+ 62 - 0
frontend-next/src/views/other/Error.vue

@@ -0,0 +1,62 @@
+<template>
+    <div class="wrapper">
+        <h1 class="title">{{ $route.meta.status_code ? $route.meta.status_code : 404 }}</h1>
+        <p>{{ $route.meta.error ? $route.meta.error : $gettext('File Not Found') }}</p>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'Error'
+}
+</script>
+
+<style lang="less" scoped>
+body, div, h1, html {
+    padding: 0;
+    margin: 0
+}
+
+body, html {
+    color: #444;
+    position: relative;
+    font-family: "PingFang SC", "Helvetica Neue", Helvetica, Arial, CustomFont, "Microsoft YaHei UI", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
+    background: #fcfcfc;
+    height: 100%
+}
+
+h1 {
+    font-size: 8em;
+    font-weight: 100
+}
+
+a {
+    color: #4181b9;
+    text-decoration: none;
+    -webkit-transition: all .3s ease;
+    -moz-transition: all .3s ease;
+    -ms-transition: all .3s ease;
+    -o-transition: all .3s ease;
+    transition: all .3s ease;
+
+    &:active, &:hover {
+        color: #5bb0ed
+    }
+
+}
+
+.wrapper {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    font-size: 1em;
+    font-weight: 400;
+    width: 100%;
+    height: 30%;
+    line-height: 1;
+    margin: auto;
+    text-align: center
+}
+</style>

+ 145 - 0
frontend-next/src/views/other/Install.vue

@@ -0,0 +1,145 @@
+<template>
+    <div class="login-form">
+        <div class="project-title">
+            <h1>Nginx UI</h1>
+        </div>
+        <a-form
+            id="components-form-install"
+            :form="form"
+            class="login-form"
+            @submit="handleSubmit"
+        >
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'email',
+          { rules: [{
+                type: 'email',
+                message: $gettext('Invalid E-mail!'),
+              },
+              {
+                required: true,
+                message: $gettext('Please input your E-mail!'),
+              },] },
+        ]"
+                    :placeholder="$gettext('Email (*)')"
+                >
+                    <a-icon slot="prefix" type="mail" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'username',
+          { rules: [{ required: true, message: $gettext('Please input your username!') }] },
+        ]"
+                    :placeholder="$gettext('Username (*)')"
+                >
+                    <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'password',
+          { rules: [{ required: true, message: $gettext('Please input your password!') }] },
+        ]"
+                    type="password"
+                    :placeholder="$gettext('Password (*)')"
+                >
+                    <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'database',
+          { rules: [{ pattern: /^[^\\/:*?\x22<>|]{1,120}$/,
+          message: $gettextInterpolate(
+              $gettext('The filename cannot contain the following characters: %{c}'),
+              {c: '& &quot; ? < > # {} % ~ / \\'}
+          )}] },
+        ]"
+                    :placeholder="$gettext('Database (Optional, default: database)')"
+                >
+                    <a-icon slot="prefix" type="database" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-button type="primary" :block="true" html-type="submit" :loading="loading">
+                    <translate>Install</translate>
+                </a-button>
+            </a-form-item>
+        </a-form>
+        <footer>
+            Copyright © 2020 - {{ thisYear }} Nginx UI | Language <set-language class="set_lang" style="display: inline"/>
+        </footer>
+    </div>
+
+</template>
+
+<script>
+import SetLanguage from "@/components/SetLanguage/SetLanguage";
+
+export default {
+    name: 'Login',
+    components: {SetLanguage},
+    data() {
+        return {
+            form: {},
+            lock: true,
+            thisYear: new Date().getFullYear(),
+            loading: false
+        }
+    },
+    created() {
+        this.form = this.$form.createForm(this)
+    },
+    mounted() {
+        this.$api.install.get_lock().then(r => {
+            if (r.lock) {
+                this.$router.push('/login')
+            }
+        })
+    },
+    methods: {
+        handleSubmit: async function (e) {
+            e.preventDefault()
+            this.loading = true
+            await this.form.validateFields(async (err, values) => {
+                if (!err) {
+                    this.$api.install.install_nginx_ui(values).then(() => {
+                        this.$router.push('/login')
+                    })
+                }
+                this.loading = false
+            })
+        },
+    },
+}
+</script>
+<style lang="less">
+.project-title {
+    margin: 50px;
+
+    h1 {
+        font-size: 50px;
+        font-weight: 100;
+        text-align: center;
+    }
+}
+
+.login-form {
+    max-width: 500px;
+    margin: 0 auto;
+}
+
+.login-form-button {
+
+}
+
+footer {
+    padding: 30px;
+    text-align: center;
+}
+</style>

+ 133 - 0
frontend-next/src/views/other/Login.vue

@@ -0,0 +1,133 @@
+<script setup lang="ts">
+const thisYear = new Date().getFullYear()
+
+import app from '@/main'
+import {UserOutlined, LockOutlined} 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 auth from '@/api/auth'
+
+const route = useRoute()
+const router = useRouter()
+
+const {$gettext} = gettext
+const loading = ref(false)
+
+const modelRef = reactive({
+    username: '',
+    password: ''
+})
+
+const rulesRef = reactive({
+    username: [
+        {
+            required: true,
+            message: $gettext('Please input your username!'),
+        }
+    ],
+    password: [
+        {
+            required: true,
+            message: $gettext('Please input your password!'),
+        }
+    ]
+})
+
+const {validate, validateInfos} = Form.useForm(modelRef, rulesRef)
+
+const onSubmit = () => {
+    validate().then(() => {
+        // modelRef
+        auth.login(modelRef.username, modelRef.password).then(async ()=>{
+            message.success($gettext('Login successful'), 1)
+            const next = (route.query?.next||'').toString() || '/'
+            await router.push(next)
+        }).
+        catch(e=>{
+            message.error(e.message)
+        })
+    })
+}
+
+</script>
+
+<template>
+    <div class="container">
+        <div class="login-form">
+            <div class="project-title">
+                <h1>Nginx UI</h1>
+            </div>
+            <a-form id="components-form-demo-normal-login">
+                <a-form-item v-bind="validateInfos.username">
+                    <a-input
+                        v-model:value="modelRef.username"
+                        :placeholder="$gettext('Username')"
+                    >
+                        <template #prefix>
+                            <UserOutlined style="color: rgba(0, 0, 0, 0.25)"/>
+                        </template>
+                    </a-input>
+                </a-form-item>
+                <a-form-item v-bind="validateInfos.password">
+                    <a-input-password
+                        v-model:value="modelRef.password"
+                        :placeholder="$gettext('Password')"
+                    >
+                        <template #prefix>
+                            <LockOutlined style="color: rgba(0, 0, 0, 0.25)"/>
+                        </template>
+                    </a-input-password>
+                </a-form-item>
+                <a-form-item>
+                    <a-button @click="onSubmit" type="primary" :block="true" html-type="submit" :loading="loading">
+                        <translate>Login</translate>
+                    </a-button>
+                </a-form-item>
+            </a-form>
+            <div class="footer">
+                <p>Copyright © 2020 - {{ thisYear }} Nginx UI</p>
+                Language <set-language class="set_lang" style="display: inline"/>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="less">
+.container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+
+    .login-form {
+        max-width: 400px;
+        width: 80%;
+
+        .project-title {
+            margin: 50px;
+
+            h1 {
+                font-size: 50px;
+                font-weight: 100;
+                text-align: center;
+            }
+        }
+
+        .anticon {
+            color: #a8a5a5 !important;
+        }
+
+        .login-form-button {
+
+        }
+
+        .footer {
+            padding: 30px;
+            text-align: center;
+        }
+    }
+}
+
+</style>

+ 110 - 0
frontend-next/src/views/pty/Terminal.vue

@@ -0,0 +1,110 @@
+<script setup lang="ts">
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import 'xterm/css/xterm.css'
+import {Terminal} from 'xterm'
+import {FitAddon} from 'xterm-addon-fit'
+import {onMounted, onUnmounted} from "vue"
+import {useUserStore} from "@/pinia/user"
+import {storeToRefs} from "pinia"
+import _ from 'lodash'
+
+const user = useUserStore()
+const {token} = storeToRefs(user)
+
+let term: Terminal | null
+let ping: null | NodeJS.Timer
+
+const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'
+
+const websocket = new ReconnectingWebSocket(
+    protocol + window.location.host + '/api/pty?token='
+    + btoa(token.value))
+
+onMounted(() => {
+    initTerm()
+
+    websocket.onmessage = wsOnMessage
+    websocket.onopen = wsOnOpen
+})
+
+interface Message {
+    Type: Number,
+    Data: any | null
+}
+
+const fitAddon = new FitAddon()
+
+const fit = _.throttle(function () {
+    fitAddon.fit()
+}, 50)
+
+function initTerm() {
+    term = new Terminal({
+        rendererType: 'canvas',
+        convertEol: true,
+        fontSize: 14,
+        cursorStyle: 'block',
+        scrollback: 1000,
+        theme: {
+            background: 'rgba(3,14,32,0.7)'
+        },
+    })
+
+    term.loadAddon(fitAddon)
+    // this.fitAddon = fitAddon
+    term.open(document.getElementById('terminal')!)
+    setTimeout(() => {
+        fitAddon.fit()
+    }, 60)
+    window.addEventListener('resize', fit)
+    term.focus()
+
+    term.onData(function (key) {
+        let order: Message = {
+            Data: key,
+            Type: 1
+        }
+        sendMessage(order)
+    })
+    term.onBinary(data => {
+        sendMessage({Type: 1, Data: data})
+    })
+    term.onResize(data => {
+        sendMessage({Type: 2, Data: {Cols: data.cols, Rows: data.rows}})
+    })
+}
+
+function sendMessage(data: Message) {
+    websocket.send(JSON.stringify(data))
+}
+
+function wsOnMessage(msg: { data: any }) {
+    term!.write(msg.data)
+}
+
+function wsOnOpen() {
+    ping = setInterval(function () {
+        sendMessage({Type: 3, Data: null})
+    }, 30000)
+}
+
+onUnmounted(() => {
+    window.removeEventListener('resize', fit)
+    clearInterval(ping!)
+    ping = null
+    websocket.close()
+})
+
+</script>
+
+<template>
+    <a-card :title="$gettext('Terminal')">
+        <div class="console" id="terminal"></div>
+    </a-card>
+</template>
+
+<style lang="less" scoped>
+.console {
+    min-height: calc(100vh - 300px);
+}
+</style>

+ 60 - 0
frontend-next/src/views/user/User.vue

@@ -0,0 +1,60 @@
+<template>
+    <std-curd :columns="columns" :api="api" :disable_search="true"/>
+</template>
+
+<script lang="ts">
+import StdCurd from '@/components/StdDataDisplay/StdCurd.vue'
+import gettext from '@/gettext'
+const {$gettext} = gettext
+
+const columns = [{
+    title: $gettext('Username'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true,
+    edit: {
+        type: 'input'
+    }
+}, {
+    title: $gettext('Password'),
+    dataIndex: 'password',
+    sorter: true,
+    pithy: true,
+    edit: {
+        type: 'input',
+        placeholder: $gettext('Leave blank for no change')
+    },
+    display: false
+}, {
+    title: $gettext('Created at'),
+    dataIndex: 'created_at',
+    datetime: true,
+    sorter: true,
+    pithy: true
+}, {
+    title: $gettext('Updated at'),
+    dataIndex: 'updated_at',
+    datetime: true,
+    sorter: true,
+    pithy: true
+}, {
+    title: $gettext('Action'),
+    dataIndex: 'action'
+}]
+
+export default {
+    name: 'User',
+    components: {StdCurd},
+    data() {
+        return {
+            api: this.$api.user,
+            columns
+        }
+    },
+    methods: {}
+}
+</script>
+
+<style scoped>
+
+</style>

+ 7 - 0
frontend-next/src/vite-env.d.ts

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

+ 34 - 0
frontend-next/tsconfig.json

@@ -0,0 +1,34 @@
+{
+    "compilerOptions": {
+        "target": "ESNext",
+        "useDefineForClassFields": true,
+        "module": "ESNext",
+        "moduleResolution": "Node",
+        "strict": true,
+        "jsx": "preserve",
+        "sourceMap": true,
+        "resolveJsonModule": true,
+        "isolatedModules": true,
+        "esModuleInterop": true,
+        "lib": [
+            "ESNext",
+            "DOM"
+        ],
+        "skipLibCheck": true,
+        "baseUrl": ".",
+        "paths": {
+            "@/*": ["./src/*"]
+        }
+    },
+    "include": [
+        "src/**/*.ts",
+        "src/**/*.d.ts",
+        "src/**/*.tsx",
+        "src/**/*.vue"
+    ],
+    "references": [
+        {
+            "path": "./tsconfig.node.json"
+        }
+    ]
+}

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

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

+ 101 - 0
frontend-next/vite.config.ts

@@ -0,0 +1,101 @@
+import {defineConfig} from 'vite'
+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 {themePreprocessorPlugin, themePreprocessorHmrPlugin} from "@zougt/vite-plugin-theme-preprocessor";
+import { fileURLToPath, URL } from "url"
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+    resolve: {
+        alias: {
+            "@": fileURLToPath(new URL("./src", import.meta.url)),
+        },
+        extensions: [
+            '.mjs',
+            '.js',
+            '.ts',
+            '.jsx',
+            '.tsx',
+            '.json',
+            '.vue',
+            '.less'
+        ]
+    },
+    plugins: [vue(),
+        Components({
+            resolvers: [AntDesignVueResolver({importStyle: false})]
+        }),
+        themePreprocessorPlugin({
+            less: {
+                multipleScopeVars: [
+                    {
+                        scopeName: "theme-default",
+                        path: path.resolve("./src/style.less"),
+                    },
+                    {
+                        scopeName: "theme-dark",
+                        path: path.resolve("./src/dark.less"),
+                    },
+                ],
+                // css中不是由主题色变量生成的颜色,也让它抽取到主题css内,可以提高权重
+                includeStyleWithColors: [
+                    {
+                        color: "#ffffff",
+                        // 排除属性
+                        // excludeCssProps:["background","background-color"]
+                        // 排除选择器
+                        // excludeSelectors: [
+                        //   ".ant-btn-link:hover, .ant-btn-link:focus, .ant-btn-link:active",
+                        // ],
+                    },
+                    {
+                        color: ["transparent","none"],
+                    },
+                ],
+            },
+        }),
+        themePreprocessorHmrPlugin(),
+        createHtmlPlugin({
+            minify: true,
+            /**
+             * After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
+             * @default src/main.ts
+             */
+            entry: 'src/main.ts',
+            /**
+             * If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
+             * @default index.html
+             */
+            template: 'index.html',
+
+            /**
+             * Data that needs to be injected into the index.html ejs template
+             */
+            inject: {
+                data: {
+                    title: 'Nginx UI',
+                },
+            },
+        }),
+    ],
+    css: {
+        preprocessorOptions: {
+            less: {
+                javascriptEnabled: true,
+            }
+        },
+    },
+    server: {
+        proxy: {
+            '/api': {
+                target: 'https://nginx.jackyu.cn/',
+                changeOrigin: true,
+                secure: false,
+                ws: true,
+            },
+        },
+    },
+})

+ 2280 - 0
frontend-next/yarn.lock

@@ -0,0 +1,2280 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ant-design/colors@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
+  integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==
+  dependencies:
+    "@ctrl/tinycolor" "^3.4.0"
+
+"@ant-design/icons-svg@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz#8630da8eb4471a4aabdaed7d1ff6a97dcb2cf05a"
+  integrity sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==
+
+"@ant-design/icons-vue@^6.1.0":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@ant-design/icons-vue/-/icons-vue-6.1.0.tgz#f9324fdc0eb4cea943cf626d2bf3db9a4ff4c074"
+  integrity sha512-EX6bYm56V+ZrKN7+3MT/ubDkvJ5rK/O2t380WFRflDcVFgsvl3NLH7Wxeau6R8DbrO5jWR6DSTC3B6gYFp77AA==
+  dependencies:
+    "@ant-design/colors" "^6.0.0"
+    "@ant-design/icons-svg" "^4.2.1"
+
+"@antfu/utils@^0.5.2":
+  version "0.5.2"
+  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":
+  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/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/highlight@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.18.6"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.16.4":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
+  integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
+
+"@babel/runtime@^7.10.5":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
+  integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@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":
+  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==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.0.3":
+  version "3.1.0"
+  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":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+  version "1.4.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
+  integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@nodelib/fs.scandir@2.1.5":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
+"@rollup/pluginutils@^4.2.0", "@rollup/pluginutils@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
+  integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
+  dependencies:
+    estree-walker "^2.0.1"
+    picomatch "^2.2.2"
+
+"@simonwep/pickr@~1.8.0":
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/@simonwep/pickr/-/pickr-1.8.2.tgz#96dc86675940d7cad63d69c22083dd1cbb9797cb"
+  integrity sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==
+  dependencies:
+    core-js "^3.15.1"
+    nanopop "^2.1.0"
+
+"@trysound/sax@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
+"@types/glob@5 - 7":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/lodash@^4.14.182":
+  version "4.14.182"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
+  integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
+
+"@types/minimatch@*":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
+
+"@types/node@*":
+  version "18.6.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.2.tgz#ffc5f0f099d27887c8d9067b54e55090fcd54126"
+  integrity sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+"@types/parse5@^5":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
+  integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
+
+"@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"
+  integrity sha512-Ll9JgxG7ONIz/XZv3dssfoMUDu9qAnlJ+km+pBA0teYSXzwPCIzS/e1bmwNYl5dcQGs677D21amgfYAnzMl17A==
+
+"@volar/code-gen@0.38.9":
+  version "0.38.9"
+  resolved "https://registry.yarnpkg.com/@volar/code-gen/-/code-gen-0.38.9.tgz#8fed2c6a472c8f11ce695b08789bcc22b08e7fa6"
+  integrity sha512-n6LClucfA+37rQeskvh9vDoZV1VvCVNy++MAPKj2dT4FT+Fbmty/SDQqnsEBtdEe6E3OQctFvA/IcKsx3Mns0A==
+  dependencies:
+    "@volar/source-map" "0.38.9"
+
+"@volar/source-map@0.38.9":
+  version "0.38.9"
+  resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-0.38.9.tgz#935d6def4b4342e8e2d63cd8e6bf9bf1155c58d8"
+  integrity sha512-ba0UFoHDYry+vwKdgkWJ6xlQT+8TFtZg1zj9tSjj4PykW1JZDuM0xplMotLun4h3YOoYfY9K1huY5gvxmrNLIw==
+
+"@volar/vue-code-gen@0.38.9":
+  version "0.38.9"
+  resolved "https://registry.yarnpkg.com/@volar/vue-code-gen/-/vue-code-gen-0.38.9.tgz#878f00fec82a2fc300396d70e26b0ea29952f740"
+  integrity sha512-tzj7AoarFBKl7e41MR006ncrEmNPHALuk8aG4WdDIaG387X5//5KhWC5Ff3ZfB2InGSeNT+CVUd74M0gS20rjA==
+  dependencies:
+    "@volar/code-gen" "0.38.9"
+    "@volar/source-map" "0.38.9"
+    "@vue/compiler-core" "^3.2.37"
+    "@vue/compiler-dom" "^3.2.37"
+    "@vue/shared" "^3.2.37"
+
+"@volar/vue-typescript@0.38.9":
+  version "0.38.9"
+  resolved "https://registry.yarnpkg.com/@volar/vue-typescript/-/vue-typescript-0.38.9.tgz#e5dfdc6f0d6dbea683647cd477fafbd483983b35"
+  integrity sha512-iJMQGU91ADi98u8V1vXd2UBmELDAaeSP0ZJaFjwosClQdKlJQYc6MlxxKfXBZisHqfbhdtrGRyaryulnYtliZw==
+  dependencies:
+    "@volar/code-gen" "0.38.9"
+    "@volar/source-map" "0.38.9"
+    "@volar/vue-code-gen" "0.38.9"
+    "@vue/compiler-sfc" "^3.2.37"
+    "@vue/reactivity" "^3.2.37"
+
+"@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"
+  integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/shared" "3.2.37"
+    estree-walker "^2.0.2"
+    source-map "^0.6.1"
+
+"@vue/compiler-dom@3.2.37", "@vue/compiler-dom@^3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5"
+  integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==
+  dependencies:
+    "@vue/compiler-core" "3.2.37"
+    "@vue/shared" "3.2.37"
+
+"@vue/compiler-sfc@3.2.37", "@vue/compiler-sfc@^3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4"
+  integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/compiler-ssr" "3.2.37"
+    "@vue/reactivity-transform" "3.2.37"
+    "@vue/shared" "3.2.37"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+    postcss "^8.1.10"
+    source-map "^0.6.1"
+
+"@vue/compiler-ssr@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff"
+  integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==
+  dependencies:
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/shared" "3.2.37"
+
+"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.1.4", "@vue/devtools-api@^6.2.1":
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092"
+  integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==
+
+"@vue/reactivity-transform@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca"
+  integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==
+  dependencies:
+    "@babel/parser" "^7.16.4"
+    "@vue/compiler-core" "3.2.37"
+    "@vue/shared" "3.2.37"
+    estree-walker "^2.0.2"
+    magic-string "^0.25.7"
+
+"@vue/reactivity@3.2.37", "@vue/reactivity@^3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.37.tgz#5bc3847ac58828e2b78526e08219e0a1089f8848"
+  integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==
+  dependencies:
+    "@vue/shared" "3.2.37"
+
+"@vue/runtime-core@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz#7ba7c54bb56e5d70edfc2f05766e1ca8519966e3"
+  integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==
+  dependencies:
+    "@vue/reactivity" "3.2.37"
+    "@vue/shared" "3.2.37"
+
+"@vue/runtime-dom@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz#002bdc8228fa63949317756fb1e92cdd3f9f4bbd"
+  integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==
+  dependencies:
+    "@vue/runtime-core" "3.2.37"
+    "@vue/shared" "3.2.37"
+    csstype "^2.6.8"
+
+"@vue/server-renderer@3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz#840a29c8dcc29bddd9b5f5ffa22b95c0e72afdfc"
+  integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==
+  dependencies:
+    "@vue/compiler-ssr" "3.2.37"
+    "@vue/shared" "3.2.37"
+
+"@vue/shared@3.2.37", "@vue/shared@^3.2.37":
+  version "3.2.37"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
+  integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
+
+"@zougt/some-loader-utils@^1.4.3":
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/@zougt/some-loader-utils/-/some-loader-utils-1.4.3.tgz#41cf762b291ab9697f8c008bdeebaf80eaee4714"
+  integrity sha512-0FsoqSTQ+qOyp6x5Q6LZQ7xVwquEgLYiIStG3L8p0Q2GsGGYKDkOZ0mIpMt67aNdr8XLsbxXjzTl/iHtTz5zcA==
+  dependencies:
+    cac "^6.7.12"
+    color "^4.0.1"
+    cssnano "^5.0.11"
+    cssnano-preset-lite "^2.0.1"
+    fs-extra "^10.0.0"
+    postcss "^8.2.9"
+    prettier "^2.5.0"
+    uuid "^8.3.2"
+
+"@zougt/vite-plugin-theme-preprocessor@^1.4.5":
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/@zougt/vite-plugin-theme-preprocessor/-/vite-plugin-theme-preprocessor-1.4.5.tgz#557b80592a5d131cb856338f0422b2df428566f9"
+  integrity sha512-pG+4Iz4rtA7AS+EZnRuoFler6SxlbL7ii6IqepQq1XWmGeZTzbRCjjYr9uteBYSdwHHW90A9gcAxhqadLXUnEg==
+  dependencies:
+    "@zougt/some-loader-utils" "^1.4.3"
+    cac "^6.7.12"
+    chalk "^5.0.0"
+    fs-extra "^10.0.0"
+    string-hash "^1.1.3"
+
+acorn@^8.5.0, acorn@^8.7.1:
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+  integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+ant-design-vue@^3.2.10:
+  version "3.2.10"
+  resolved "https://registry.yarnpkg.com/ant-design-vue/-/ant-design-vue-3.2.10.tgz#938260177126cf7ab0dc476dd9e3e9d06ab17bae"
+  integrity sha512-aqa0kjJzVQ74MfVw5w7rTOdJQL2JN9V/O6Ro+VQQMq/tY7q91JiomhI9TRKAK3tFdBDXJpUoBCVOsosbbxMzRw==
+  dependencies:
+    "@ant-design/colors" "^6.0.0"
+    "@ant-design/icons-vue" "^6.1.0"
+    "@babel/runtime" "^7.10.5"
+    "@ctrl/tinycolor" "^3.4.0"
+    "@simonwep/pickr" "~1.8.0"
+    array-tree-filter "^2.1.0"
+    async-validator "^4.0.0"
+    dayjs "^1.10.5"
+    dom-align "^1.12.1"
+    dom-scroll-into-view "^2.0.0"
+    lodash "^4.17.21"
+    lodash-es "^4.17.15"
+    resize-observer-polyfill "^1.5.1"
+    scroll-into-view-if-needed "^2.2.25"
+    shallow-equal "^1.0.0"
+    vue-types "^3.0.0"
+    warning "^4.0.0"
+
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+array-back@^3.0.1, array-back@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
+  integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
+
+array-tree-filter@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz#873ac00fec83749f255ac8dd083814b4f6329190"
+  integrity sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==
+
+async-validator@^4.0.0:
+  version "4.2.5"
+  resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
+  integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
+
+async@^3.2.3:
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
+  integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^0.27.2:
+  version "0.27.2"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
+  integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
+  dependencies:
+    follow-redirects "^1.14.9"
+    form-data "^4.0.0"
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.20.3:
+  version "4.21.3"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
+  integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
+  dependencies:
+    caniuse-lite "^1.0.30001370"
+    electron-to-chromium "^1.4.202"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.5"
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+cac@^6.7.12:
+  version "6.7.12"
+  resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.12.tgz#6fb5ea2ff50bd01490dbda497f4ae75a99415193"
+  integrity sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camel-case@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
+  integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
+  dependencies:
+    pascal-case "^3.1.2"
+    tslib "^2.0.3"
+
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001370:
+  version "1.0.30001373"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz#2dc3bc3bfcb5d5a929bec11300883040d7b4b4be"
+  integrity sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ==
+
+chalk@^2.0.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^4.0.2, chalk@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chalk@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
+  integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
+
+chokidar@^3.5.3:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+clean-css@^5.2.2:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.1.tgz#d0610b0b90d125196a2894d35366f734e5d7aa32"
+  integrity sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==
+  dependencies:
+    source-map "~0.6.0"
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+  integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
+color@^4.0.1:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
+  integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
+  dependencies:
+    color-convert "^2.0.1"
+    color-string "^1.9.0"
+
+colord@^2.9.1:
+  version "2.9.2"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
+  integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==
+
+colorette@^2.0.16:
+  version "2.0.19"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
+  integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+command-line-args@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e"
+  integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==
+  dependencies:
+    array-back "^3.1.0"
+    find-replace "^3.0.0"
+    lodash.camelcase "^4.3.0"
+    typical "^4.0.0"
+
+commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+commander@^8.3.0:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
+  integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+
+compute-scroll-into-view@^1.0.17:
+  version "1.0.17"
+  resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
+  integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+connect-history-api-fallback@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
+  integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
+
+consola@^2.15.3:
+  version "2.15.3"
+  resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
+  integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
+
+copy-anything@^2.0.1:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
+  integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==
+  dependencies:
+    is-what "^3.14.1"
+
+core-js@^3.15.1:
+  version "3.24.0"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57"
+  integrity sha512-IeOyT8A6iK37Ep4kZDD423mpi6JfPRoPUdQwEWYiGolvn4o6j2diaRzNfDfpTdu3a5qMbrGUzKUpYpRY8jXCkQ==
+
+cosmiconfig@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
+  integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.2.1"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.10.0"
+
+css-declaration-sorter@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.0.tgz#72ebd995c8f4532ff0036631f7365cce9759df14"
+  integrity sha512-OGT677UGHJTAVMRhPO+HJ4oKln3wkBTwtDFH0ojbqm+MJm6xuDMHp2nkhh/ThaBqq20IbraBQSWKfSLNHQO9Og==
+
+css-select@^4.1.3, css-select@^4.2.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
+  integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^6.0.1"
+    domhandler "^4.3.1"
+    domutils "^2.8.0"
+    nth-check "^2.0.1"
+
+css-selector-parser@^1.3:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.4.1.tgz#03f9cb8a81c3e5ab2c51684557d5aaf6d2569759"
+  integrity sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
+  integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^5.2.12:
+  version "5.2.12"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.12.tgz#ebe6596ec7030e62c3eb2b3c09f533c0644a9a97"
+  integrity sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew==
+  dependencies:
+    css-declaration-sorter "^6.3.0"
+    cssnano-utils "^3.1.0"
+    postcss-calc "^8.2.3"
+    postcss-colormin "^5.3.0"
+    postcss-convert-values "^5.1.2"
+    postcss-discard-comments "^5.1.2"
+    postcss-discard-duplicates "^5.1.0"
+    postcss-discard-empty "^5.1.1"
+    postcss-discard-overridden "^5.1.0"
+    postcss-merge-longhand "^5.1.6"
+    postcss-merge-rules "^5.1.2"
+    postcss-minify-font-values "^5.1.0"
+    postcss-minify-gradients "^5.1.1"
+    postcss-minify-params "^5.1.3"
+    postcss-minify-selectors "^5.2.1"
+    postcss-normalize-charset "^5.1.0"
+    postcss-normalize-display-values "^5.1.0"
+    postcss-normalize-positions "^5.1.1"
+    postcss-normalize-repeat-style "^5.1.1"
+    postcss-normalize-string "^5.1.0"
+    postcss-normalize-timing-functions "^5.1.0"
+    postcss-normalize-unicode "^5.1.0"
+    postcss-normalize-url "^5.1.0"
+    postcss-normalize-whitespace "^5.1.1"
+    postcss-ordered-values "^5.1.3"
+    postcss-reduce-initial "^5.1.0"
+    postcss-reduce-transforms "^5.1.0"
+    postcss-svgo "^5.1.0"
+    postcss-unique-selectors "^5.1.1"
+
+cssnano-preset-lite@^2.0.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-lite/-/cssnano-preset-lite-2.1.3.tgz#da458cd8749443eb22f4253f187fd1dafa73547a"
+  integrity sha512-samvnCll/DUVZu0Qc+JH36nt7dlaOT7WjOgg8SbLJ78sp51JZ12s2hyerxrarjPBG4O53rErUtOY2IYLYgBGEQ==
+  dependencies:
+    cssnano-utils "^3.1.0"
+    postcss-discard-comments "^5.1.2"
+    postcss-discard-empty "^5.1.1"
+    postcss-normalize-whitespace "^5.1.1"
+
+cssnano-utils@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861"
+  integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==
+
+cssnano@^5.0.11:
+  version "5.1.12"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.12.tgz#bcd0b64d6be8692de79332c501daa7ece969816c"
+  integrity sha512-TgvArbEZu0lk/dvg2ja+B7kYoD7BBCmn3+k58xD0qjrGHsFzXY/wKTo9M5egcUCabPol05e/PVoIu79s2JN4WQ==
+  dependencies:
+    cssnano-preset-default "^5.2.12"
+    lilconfig "^2.0.3"
+    yaml "^1.10.2"
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
+csstype@^2.6.8:
+  version "2.6.20"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
+  integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
+
+dayjs@^1.10.5:
+  version "1.11.4"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e"
+  integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==
+
+debug@^3.2.6:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
+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==
+  dependencies:
+    ms "2.1.2"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+dom-align@^1.12.1:
+  version "1.12.3"
+  resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.3.tgz#a36d02531dae0eefa2abb0c4db6595250526f103"
+  integrity sha512-Gj9hZN3a07cbR6zviMUBOMPdWxYhbMI+x+WS0NAIu2zFZmbK8ys9R79g+iG9qLnlCwpFoaB+fKy8Pdv470GsPA==
+
+dom-scroll-into-view@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz#0decc8522801fd8d3f1c6ba355a74d382c5f989b"
+  integrity sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==
+
+dom-serializer@^1.0.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
+  integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
+domhandler@^4.2.0, domhandler@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+  integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
+dot-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
+  integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
+  dependencies:
+    no-case "^3.0.4"
+    tslib "^2.0.3"
+
+dotenv-expand@^8.0.2:
+  version "8.0.3"
+  resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
+  integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
+
+dotenv@^16.0.0:
+  version "16.0.1"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
+  integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
+
+ejs@^3.1.6:
+  version "3.1.8"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b"
+  integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==
+  dependencies:
+    jake "^10.8.5"
+
+electron-to-chromium@^1.4.202:
+  version "1.4.206"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.206.tgz#580ff85b54d7ec0c05f20b1e37ea0becdd7b0ee4"
+  integrity sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA==
+
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
+errno@^0.1.1:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+  integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
+  dependencies:
+    prr "~1.0.1"
+
+error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+  dependencies:
+    is-arrayish "^0.2.1"
+
+esbuild-android-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.51.tgz#414a087cb0de8db1e347ecca6c8320513de433db"
+  integrity sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==
+
+esbuild-android-arm64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.51.tgz#55de3bce2aab72bcd2b606da4318ad00fb9c8151"
+  integrity sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==
+
+esbuild-darwin-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.51.tgz#4259f23ed6b4cea2ec8a28d87b7fb9801f093754"
+  integrity sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==
+
+esbuild-darwin-arm64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.51.tgz#d77b4366a71d84e530ba019d540b538b295d494a"
+  integrity sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==
+
+esbuild-freebsd-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.51.tgz#27b6587b3639f10519c65e07219d249b01f2ad38"
+  integrity sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==
+
+esbuild-freebsd-arm64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.51.tgz#63c435917e566808c71fafddc600aca4d78be1ec"
+  integrity sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==
+
+esbuild-linux-32@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.51.tgz#c3da774143a37e7f11559b9369d98f11f997a5d9"
+  integrity sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==
+
+esbuild-linux-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.51.tgz#5d92b67f674e02ae0b4a9de9a757ba482115c4ae"
+  integrity sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==
+
+esbuild-linux-arm64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.51.tgz#dac84740516e859d8b14e1ecc478dd5241b10c93"
+  integrity sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==
+
+esbuild-linux-arm@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.51.tgz#b3ae7000696cd53ed95b2b458554ff543a60e106"
+  integrity sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==
+
+esbuild-linux-mips64le@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.51.tgz#dad10770fac94efa092b5a0643821c955a9dd385"
+  integrity sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==
+
+esbuild-linux-ppc64le@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.51.tgz#b68c2f8294d012a16a88073d67e976edd4850ae0"
+  integrity sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==
+
+esbuild-linux-riscv64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.51.tgz#608a318b8697123e44c1e185cdf6708e3df50b93"
+  integrity sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==
+
+esbuild-linux-s390x@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.51.tgz#c9e7791170a3295dba79b93aa452beb9838a8625"
+  integrity sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==
+
+esbuild-netbsd-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.51.tgz#0abd40b8c2e37fda6f5cc41a04cb2b690823d891"
+  integrity sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==
+
+esbuild-openbsd-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.51.tgz#4adba0b7ea7eb1428bb00d8e94c199a949b130e8"
+  integrity sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==
+
+esbuild-sunos-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.51.tgz#4b8a6d97dfedda30a6e39607393c5c90ebf63891"
+  integrity sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==
+
+esbuild-windows-32@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.51.tgz#d31d8ca0c1d314fb1edea163685a423b62e9ac17"
+  integrity sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==
+
+esbuild-windows-64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.51.tgz#7d3c09c8652d222925625637bdc7e6c223e0085d"
+  integrity sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==
+
+esbuild-windows-arm64@0.14.51:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.51.tgz#0220d2304bfdc11bc27e19b2aaf56edf183e4ae9"
+  integrity sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==
+
+esbuild@^0.14.47:
+  version "0.14.51"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.51.tgz#1c8ecbc8db3710da03776211dc3ee3448f7aa51e"
+  integrity sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==
+  optionalDependencies:
+    esbuild-android-64 "0.14.51"
+    esbuild-android-arm64 "0.14.51"
+    esbuild-darwin-64 "0.14.51"
+    esbuild-darwin-arm64 "0.14.51"
+    esbuild-freebsd-64 "0.14.51"
+    esbuild-freebsd-arm64 "0.14.51"
+    esbuild-linux-32 "0.14.51"
+    esbuild-linux-64 "0.14.51"
+    esbuild-linux-arm "0.14.51"
+    esbuild-linux-arm64 "0.14.51"
+    esbuild-linux-mips64le "0.14.51"
+    esbuild-linux-ppc64le "0.14.51"
+    esbuild-linux-riscv64 "0.14.51"
+    esbuild-linux-s390x "0.14.51"
+    esbuild-netbsd-64 "0.14.51"
+    esbuild-openbsd-64 "0.14.51"
+    esbuild-sunos-64 "0.14.51"
+    esbuild-windows-32 "0.14.51"
+    esbuild-windows-64 "0.14.51"
+    esbuild-windows-arm64 "0.14.51"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+estree-walker@^2.0.1, estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+fast-glob@^3.2.11:
+  version "3.2.11"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
+  integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.2"
+    merge2 "^1.3.0"
+    micromatch "^4.0.4"
+
+fastq@^1.6.0:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+  integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
+  dependencies:
+    reusify "^1.0.4"
+
+filelist@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
+  integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
+  dependencies:
+    minimatch "^5.0.1"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-replace@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
+  integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==
+  dependencies:
+    array-back "^3.0.1"
+
+follow-redirects@^1.14.9:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
+  integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
+
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+fs-extra@^10.0.0, fs-extra@^10.0.1:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
+  integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^6.0.1"
+    universalify "^2.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+gettext-extractor@^3.5.4:
+  version "3.5.4"
+  resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.5.4.tgz#bd36c65b4d26014ffd925f9ac7b4738d6893d6b2"
+  integrity sha512-iK4tSnteSw+pFMts43OP8hUnsOklbkxz3ytWqru7dPf8Ec3uzTYv1aw70ojAvKItmofpj1ibfY7sZWsdSN6zIw==
+  dependencies:
+    "@types/glob" "5 - 7"
+    "@types/parse5" "^5"
+    css-selector-parser "^1.3"
+    glob "5 - 7"
+    parse5 "5 - 6"
+    pofile "1.0.x"
+    typescript "2 - 4"
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+"glob@5 - 7", glob@^7.2.0:
+  version "7.2.3"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.1.1"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+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"
+  integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+he@1.2.0, he@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+html-minifier-terser@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab"
+  integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==
+  dependencies:
+    camel-case "^4.1.2"
+    clean-css "^5.2.2"
+    commander "^8.3.0"
+    he "^1.2.0"
+    param-case "^3.0.4"
+    relateurl "^0.2.7"
+    terser "^5.10.0"
+
+iconv-lite@^0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
+image-size@~0.5.0:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
+  integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
+
+import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+  integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
+  integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-object@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b"
+  integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
+
+is-what@^3.14.1:
+  version "3.14.1"
+  resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
+  integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
+
+jake@^10.8.5:
+  version "10.8.5"
+  resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
+  integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==
+  dependencies:
+    async "^3.2.3"
+    chalk "^4.0.2"
+    filelist "^1.0.1"
+    minimatch "^3.0.4"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+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==
+
+jsonfile@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+  integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+  dependencies:
+    universalify "^2.0.0"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+less@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/less/-/less-4.1.3.tgz#175be9ddcbf9b250173e0a00b4d6920a5b770246"
+  integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==
+  dependencies:
+    copy-anything "^2.0.1"
+    parse-node-version "^1.0.1"
+    tslib "^2.3.0"
+  optionalDependencies:
+    errno "^0.1.1"
+    graceful-fs "^4.1.2"
+    image-size "~0.5.0"
+    make-dir "^2.1.0"
+    mime "^1.4.1"
+    needle "^3.1.0"
+    source-map "~0.6.0"
+
+lilconfig@^2.0.3:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
+  integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
+
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+local-pkg@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.2.tgz#13107310b77e74a0e513147a131a2ba288176c2f"
+  integrity sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==
+
+lodash-es@^4.17.15:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+lodash.camelcase@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+  integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
+
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
+
+lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+loose-envify@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+lower-case@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
+  integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+  dependencies:
+    tslib "^2.0.3"
+
+magic-string@^0.25.7:
+  version "0.25.9"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
+  integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
+  dependencies:
+    sourcemap-codec "^1.4.8"
+
+magic-string@^0.26.2:
+  version "0.26.2"
+  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432"
+  integrity sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==
+  dependencies:
+    sourcemap-codec "^1.4.8"
+
+make-dir@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+  integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+  dependencies:
+    pify "^4.0.1"
+    semver "^5.6.0"
+
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+mime@^1.4.1:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+minimatch@^3.0.4, minimatch@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimatch@^5.0.1, minimatch@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7"
+  integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
+  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"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nanoid@^3.3.4:
+  version "3.3.4"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+
+nanopop@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/nanopop/-/nanopop-2.1.0.tgz#23476513cee2405888afd2e8a4b54066b70b9e60"
+  integrity sha512-jGTwpFRexSH+fxappnGQtN9dspgE2ipa1aOjtR24igG0pv6JCxImIAmrLRHX+zUF5+1wtsFVbKyfP51kIGAVNw==
+
+needle@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/needle/-/needle-3.1.0.tgz#3bf5cd090c28eb15644181ab6699e027bd6c53c9"
+  integrity sha512-gCE9weDhjVGCRqS8dwDR/D3GTAeyXLXuqp7I8EzH6DllZGXSUyxuqqLh+YX9rMAWaaTFyVAg6rHGL25dqvczKw==
+  dependencies:
+    debug "^3.2.6"
+    iconv-lite "^0.6.3"
+    sax "^1.2.4"
+
+no-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
+  integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+  dependencies:
+    lower-case "^2.0.2"
+    tslib "^2.0.3"
+
+node-html-parser@^5.3.3:
+  version "5.3.3"
+  resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.3.3.tgz#2845704f3a7331a610e0e551bf5fa02b266341b6"
+  integrity sha512-ncg1033CaX9UexbyA7e1N0aAoAYRDiV8jkTvzEnfd1GDvzFdrsXLzR4p4ik8mwLgnaKP/jyUFWDy9q3jvRT2Jw==
+  dependencies:
+    css-select "^4.2.1"
+    he "1.2.0"
+
+node-releases@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
+  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-url@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
+nth-check@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+  dependencies:
+    boolbase "^1.0.0"
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+param-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
+  integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
+  dependencies:
+    dot-case "^3.0.4"
+    tslib "^2.0.3"
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+parse-node-version@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
+  integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
+
+parse5-htmlparser2-tree-adapter@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
+  dependencies:
+    parse5 "^6.0.1"
+
+"parse5@5 - 6", parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
+pascal-case@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
+  integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
+  dependencies:
+    no-case "^3.0.4"
+    tslib "^2.0.3"
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+path@^0.12.7:
+  version "0.12.7"
+  resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+  integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
+  dependencies:
+    process "^0.11.1"
+    util "^0.10.3"
+
+pathe@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/pathe/-/pathe-0.2.0.tgz#30fd7bbe0a0d91f0e60bae621f5d19e9e225c339"
+  integrity sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinia-plugin-persistedstate@^1.6.3:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-1.6.3.tgz#6cd691f96814603c70ec5bb756f9f4d037b1aec8"
+  integrity sha512-vwxUca3DZKW6+wnGsgu6hA0ESVKoLovF8vH1jMOPBhaH4VpCSTgn5AsprTxXyg3uMk047m0B+NggeMTcCC8H6w==
+
+pinia@^2.0.17:
+  version "2.0.17"
+  resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.17.tgz#f925e5e4f73c15e16dfb4838176a9ca50752f26b"
+  integrity sha512-AtwLwEWQgIjofjgeFT+nxbnK5lT2QwQjaHNEDqpsi2AiCwf/NY78uWTeHUyEhiiJy8+sBmw0ujgQMoQbWiZDfA==
+  dependencies:
+    "@vue/devtools-api" "^6.2.1"
+    vue-demi "*"
+
+pofile@1.0.x:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954"
+  integrity sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==
+
+pofile@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.3.tgz#e2c0d4052b9829f171b888bfb35c87791dbea297"
+  integrity sha512-sk96pUvpNwDV6PLrnhr68Uu1S5NohsxqLKz0GuracgrDo40BdF/r1RhHnjakUk6Q4Z0OKIybOQ7GevLKGN1iYw==
+
+postcss-calc@^8.2.3:
+  version "8.2.4"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5"
+  integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==
+  dependencies:
+    postcss-selector-parser "^6.0.9"
+    postcss-value-parser "^4.2.0"
+
+postcss-colormin@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a"
+  integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    colord "^2.9.1"
+    postcss-value-parser "^4.2.0"
+
+postcss-convert-values@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz#31586df4e184c2e8890e8b34a0b9355313f503ab"
+  integrity sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==
+  dependencies:
+    browserslist "^4.20.3"
+    postcss-value-parser "^4.2.0"
+
+postcss-discard-comments@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696"
+  integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==
+
+postcss-discard-duplicates@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848"
+  integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==
+
+postcss-discard-empty@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c"
+  integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==
+
+postcss-discard-overridden@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e"
+  integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==
+
+postcss-merge-longhand@^5.1.6:
+  version "5.1.6"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.6.tgz#f378a8a7e55766b7b644f48e5d8c789ed7ed51ce"
+  integrity sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+    stylehacks "^5.1.0"
+
+postcss-merge-rules@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz#7049a14d4211045412116d79b751def4484473a5"
+  integrity sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    cssnano-utils "^3.1.0"
+    postcss-selector-parser "^6.0.5"
+
+postcss-minify-font-values@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b"
+  integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-minify-gradients@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c"
+  integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==
+  dependencies:
+    colord "^2.9.1"
+    cssnano-utils "^3.1.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-minify-params@^5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.3.tgz#ac41a6465be2db735099bbd1798d85079a6dc1f9"
+  integrity sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg==
+  dependencies:
+    browserslist "^4.16.6"
+    cssnano-utils "^3.1.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-minify-selectors@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6"
+  integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==
+  dependencies:
+    postcss-selector-parser "^6.0.5"
+
+postcss-normalize-charset@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed"
+  integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==
+
+postcss-normalize-display-values@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8"
+  integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-positions@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92"
+  integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-repeat-style@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2"
+  integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-string@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228"
+  integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-timing-functions@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb"
+  integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-unicode@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz#3d23aede35e160089a285e27bf715de11dc9db75"
+  integrity sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==
+  dependencies:
+    browserslist "^4.16.6"
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-url@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc"
+  integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==
+  dependencies:
+    normalize-url "^6.0.1"
+    postcss-value-parser "^4.2.0"
+
+postcss-normalize-whitespace@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa"
+  integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-ordered-values@^5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38"
+  integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==
+  dependencies:
+    cssnano-utils "^3.1.0"
+    postcss-value-parser "^4.2.0"
+
+postcss-reduce-initial@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz#fc31659ea6e85c492fb2a7b545370c215822c5d6"
+  integrity sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+
+postcss-reduce-transforms@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9"
+  integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+
+postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
+  version "6.0.10"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
+  integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-svgo@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d"
+  integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==
+  dependencies:
+    postcss-value-parser "^4.2.0"
+    svgo "^2.7.0"
+
+postcss-unique-selectors@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6"
+  integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==
+  dependencies:
+    postcss-selector-parser "^6.0.5"
+
+postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.1.10, postcss@^8.2.9, postcss@^8.4.14:
+  version "8.4.14"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
+  integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
+  dependencies:
+    nanoid "^3.3.4"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+prettier@^2.5.0:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
+  integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
+
+process@^0.11.1:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+prr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+  integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
+
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+reconnecting-websocket@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+  integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+
+regenerator-runtime@^0.13.4:
+  version "0.13.9"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
+  integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+
+relateurl@^0.2.7:
+  version "0.2.7"
+  resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+  integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
+
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.22.1:
+  version "1.22.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+  integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+  dependencies:
+    is-core-module "^2.9.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rollup@^2.75.6:
+  version "2.77.2"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.2.tgz#6b6075c55f9cc2040a5912e6e062151e42e2c4e3"
+  integrity sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
+"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"
+  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+scroll-into-view-if-needed@^2.2.25:
+  version "2.2.29"
+  resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
+  integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==
+  dependencies:
+    compute-scroll-into-view "^1.0.17"
+
+semver@^5.6.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+shallow-equal@^1.0.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
+  integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
+  dependencies:
+    is-arrayish "^0.3.1"
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+source-map-support@~0.5.20:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+sourcemap-codec@^1.4.8:
+  version "1.4.8"
+  resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
+  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+string-hash@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
+  integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==
+
+stylehacks@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520"
+  integrity sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==
+  dependencies:
+    browserslist "^4.16.6"
+    postcss-selector-parser "^6.0.4"
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "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==
+
+svgo@^2.7.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
+  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
+  dependencies:
+    "@trysound/sax" "0.2.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    picocolors "^1.0.0"
+    stable "^0.1.8"
+
+terser@^5.10.0:
+  version "5.14.2"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10"
+  integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.2"
+    acorn "^8.5.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
+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"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+tslib@^2.0.3, tslib@^2.3.0, tslib@^2.3.1:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
+  integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
+
+"typescript@2 - 4", typescript@^4.6.4:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
+
+typical@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
+  integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
+
+universalify@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+  integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
+unplugin-vue-components@^0.21.2:
+  version "0.21.2"
+  resolved "https://registry.yarnpkg.com/unplugin-vue-components/-/unplugin-vue-components-0.21.2.tgz#d5b04b05e0521aa71fdfdba0b4ca392e3caa964d"
+  integrity sha512-HBU+EuesDj/HRs7EtYH7gBACljVhqLylltrCLModRmCToIIrrNvMh54aylUt4AD4qiwylgOx4Vgb9sBlrIcRDw==
+  dependencies:
+    "@antfu/utils" "^0.5.2"
+    "@rollup/pluginutils" "^4.2.1"
+    chokidar "^3.5.3"
+    debug "^4.3.4"
+    fast-glob "^3.2.11"
+    local-pkg "^0.4.2"
+    magic-string "^0.26.2"
+    minimatch "^5.1.0"
+    resolve "^1.22.1"
+    unplugin "^0.7.2"
+
+unplugin@^0.7.2:
+  version "0.7.2"
+  resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-0.7.2.tgz#4127012fdc2c84ea4ce03ce75e3d4f54ea47bba1"
+  integrity sha512-m7thX4jP8l5sETpLdUASoDOGOcHaOVtgNyrYlToyQUvILUtEzEnngRBrHnAX3IKqooJVmXpoa/CwQ/QqzvGaHQ==
+  dependencies:
+    acorn "^8.7.1"
+    chokidar "^3.5.3"
+    webpack-sources "^3.2.3"
+    webpack-virtual-modules "^0.4.4"
+
+update-browserslist-db@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38"
+  integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+util@^0.10.3:
+  version "0.10.4"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+  integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+  dependencies:
+    inherits "2.0.3"
+
+uuid@^8.3.2:
+  version "8.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+vite-plugin-html@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-3.2.0.tgz#0d4df9900642a321a139f1c25c05195ba9d0ec79"
+  integrity sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==
+  dependencies:
+    "@rollup/pluginutils" "^4.2.0"
+    colorette "^2.0.16"
+    connect-history-api-fallback "^1.6.0"
+    consola "^2.15.3"
+    dotenv "^16.0.0"
+    dotenv-expand "^8.0.2"
+    ejs "^3.1.6"
+    fast-glob "^3.2.11"
+    fs-extra "^10.0.1"
+    html-minifier-terser "^6.1.0"
+    node-html-parser "^5.3.3"
+    pathe "^0.2.0"
+
+vite@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.3.tgz#c7b2ed9505a36a04be1d5d23aea4ea6fc028043f"
+  integrity sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==
+  dependencies:
+    esbuild "^0.14.47"
+    postcss "^8.4.14"
+    resolve "^1.22.1"
+    rollup "^2.75.6"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+vue-apexcharts@^1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/vue-apexcharts/-/vue-apexcharts-1.6.2.tgz#0547826067f97e8ea67ca9423e524eb6669746ad"
+  integrity sha512-9HS3scJwWgKjmkcWIf+ndNDR0WytUJD8Ju0V2ZYcjYtlTLwJAf2SKUlBZaQTkDmwje/zMgulvZRi+MXmi+WkKw==
+
+vue-chartjs@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-4.1.1.tgz#b1ffc2845e09d14cb5255305b11bd3e8df8058ab"
+  integrity sha512-rKIQ3jPrjhwxjKdNJppnYxRuBSrx4QeM3nNHsfIxEqjX6QS4Jq6e6vnZBxh2MDpURDC2uvuI2N0eIt1cWXbBVA==
+
+vue-demi@*:
+  version "0.13.6"
+  resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.6.tgz#f9433cbd75e68a970dec066647f4ba6c08ced48f"
+  integrity sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==
+
+vue-router@4:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.3.tgz#f8dc7931a2253cc5aa9b740f8b98969d08ca283c"
+  integrity sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA==
+  dependencies:
+    "@vue/devtools-api" "^6.1.4"
+
+vue-tsc@^0.38.4:
+  version "0.38.9"
+  resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-0.38.9.tgz#9e945937667f704325328db8af1cc6bc7314b85e"
+  integrity sha512-Yoy5phgvGqyF98Fb4mYqboR4Q149jrdcGv5kSmufXJUq++RZJ2iMVG0g6zl+v3t4ORVWkQmRpsV4x2szufZ0LQ==
+  dependencies:
+    "@volar/vue-typescript" "0.38.9"
+
+vue-types@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/vue-types/-/vue-types-3.0.2.tgz#ec16e05d412c038262fc1efa4ceb9647e7fb601d"
+  integrity sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==
+  dependencies:
+    is-plain-object "3.0.1"
+
+vue3-gettext@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/vue3-gettext/-/vue3-gettext-2.3.0.tgz#5825949e3978aa576e035128de46c97e59b65a5b"
+  integrity sha512-06zNnOGsGxdX8BT675eOipxKX6lwfS1D3X2nJBBE26GpMLHhqUIuKxU8gkXwOFnXDnaoqphitWnV9R/qV2Sl/Q==
+  dependencies:
+    chalk "^4.1.2"
+    command-line-args "^5.2.1"
+    cosmiconfig "^7.0.1"
+    gettext-extractor "^3.5.4"
+    glob "^7.2.0"
+    parse5 "^6.0.1"
+    parse5-htmlparser2-tree-adapter "^6.0.1"
+    pofile "^1.1.3"
+    tslib "^2.3.1"
+
+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==
+  dependencies:
+    "@vue/compiler-dom" "3.2.37"
+    "@vue/compiler-sfc" "3.2.37"
+    "@vue/runtime-dom" "3.2.37"
+    "@vue/server-renderer" "3.2.37"
+    "@vue/shared" "3.2.37"
+
+vuex@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.2.tgz#f896dbd5bf2a0e963f00c67e9b610de749ccacc9"
+  integrity sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==
+  dependencies:
+    "@vue/devtools-api" "^6.0.0-beta.11"
+
+warning@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
+  integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
+  dependencies:
+    loose-envify "^1.0.0"
+
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack-virtual-modules@^0.4.4:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.4.tgz#a19fcf371923c59c4712d63d7d194b1e4d8262cc"
+  integrity sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+xterm-addon-attach@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz#220c23addd62ab88c9914e2d4c06f7407e44680e"
+  integrity sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==
+
+xterm-addon-fit@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
+  integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
+
+xterm@^4.19.0:
+  version "4.19.0"
+  resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d"
+  integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==
+
+yaml@^1.10.0, yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==

+ 2 - 2
frontend/.env.development

@@ -1,2 +1,2 @@
-VUE_APP_API_ROOT = /
-VUE_APP_API_WSS_ROOT = wss://nginx.jackyu.cn/api
+VITE_API_ROOT = /api
+VITE_API_WSS_ROOT = wss://nginx.jackyu.cn/api

+ 19 - 0
frontend/conversion.log

@@ -0,0 +1,19 @@
+--------------------------------------------------
+conversion items successful conversion: 
+
+╔════════╤═════════════════════════════════════╤══════════════════╗
+║ Number │ Conversion item                     │ Conversion count ║
+╟────────┼─────────────────────────────────────┼──────────────────╢
+║  B01   │ add package.json                    │        1         ║
+║  B02   │ add index.html                      │        1         ║
+║  B03   │ add vite.config.js                  │        1         ║
+║  B04   │ required plugins                    │        1         ║
+║  B11   │ html-webpack-plugin is supported    │        1         ║
+║  V01   │ base public path                    │        1         ║
+║  V02   │ css options                         │        1         ║
+║  V03   │ server options                      │        2         ║
+║  V05   │ resolve.alias options               │        1         ║
+║  V06   │ client-side env variables           │        1         ║
+║  V08   │ transform functional webpack config │        3         ║
+╚════════╧═════════════════════════════════════╧══════════════════╝
+

+ 24 - 0
frontend/index.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="">
+<head>
+    <meta charset="utf-8">
+    <meta content="IE=edge" http-equiv="X-UA-Compatible">
+    <meta content="width=device-width,initial-scale=1.0,user-scalable=0" name="viewport">
+    <link href="/favicon.ico" rel="icon">
+    <title>Nginx UI</title>
+    <style type="text/css">
+        body {
+            width: 100% !important;
+        }
+    </style>
+</head>
+<body>
+<noscript>
+    <strong>We're sorry but Nginx UI doesn't work properly without JavaScript enabled.
+        Please enable it to continue.</strong>
+</noscript>
+<div id="app"></div>
+<!-- built files will be auto injected -->
+<script type="module" src="/src/main.js"></script>
+</body>
+</html>

+ 87 - 76
frontend/package.json

@@ -1,80 +1,91 @@
 {
-    "name": "nginx-ui-frontend",
-    "version": "1.5.0",
-    "private": true,
-    "scripts": {
-        "serve": "vue-cli-service serve --port 8021",
-        "build": "vue-cli-service build --dest dist --modern",
-        "lint": "vue-cli-service lint"
+  "name": "nginx-ui-frontend",
+  "version": "1.5.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve --port 8021",
+    "build": "vue-cli-service build --dest dist --modern",
+    "lint": "vue-cli-service lint",
+    "serve-vite": "vite",
+    "build-vite": "vite build",
+    "preview-vite": "vite preview"
+  },
+  "dependencies": {
+    "@vitejs/plugin-react": "^2.0.0",
+    "ant-design-vue": "^1.7.3",
+    "apexcharts": "^3.33.1",
+    "axios": "^0.21.2",
+    "brace": "https://github.com/nightwing/brace",
+    "chart.js": "^2.9.4",
+    "core-js": "^3.9.0",
+    "less": "^3.11.1",
+    "less-loader": "^5.0.0",
+    "lodash": "^4.17.21",
+    "moment": "^2.24.0",
+    "node-sass": "^6.0.1",
+    "nprogress": "^0.2.0",
+    "reconnecting-websocket": "^4.4.0",
+    "vite-plugin-antdv1-momentjs-resolver": "^1.1.1",
+    "vue": "^2.7.8",
+    "vue-apexcharts": "^1.6.2",
+    "vue-chartjs": "^3.5.1",
+    "vue-cli-plugin-generate-build-id": "^0.2.0",
+    "vue-codemirror": "^4.0.6",
+    "vue-cropper": "^0.5.8",
+    "vue-gettext": "^2.1.12",
+    "vue-itextarea": "^1.0.9",
+    "vue-router": "^3.5.1",
+    "vue-template-babel-compiler": "^1.1.3",
+    "vue-template-compiler": "^2.7.8",
+    "vue2-ace-editor": "^0.0.15",
+    "vuex": "^3.6.2",
+    "vuex-persist": "^3.1.3",
+    "xterm": "^4.19.0",
+    "xterm-addon-attach": "^0.6.0",
+    "xterm-addon-fit": "^0.5.0"
+  },
+  "devDependencies": {
+    "@originjs/vite-plugin-commonjs": "^1.0.1",
+    "@vue/cli-plugin-babel": "~4.5.15",
+    "@vue/cli-plugin-eslint": "~4.5.15",
+    "@vue/cli-plugin-router": "~4.5.15",
+    "@vue/cli-plugin-vuex": "~4.5.15",
+    "@vue/cli-service": "~4.5.0",
+    "babel-eslint": "^10.1.0",
+    "babel-plugin-import": "^1.13.3",
+    "easygettext": "^2.17.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-vue": "^6.2.2",
+    "vite": "^3.0.3",
+    "vite-plugin-env-compatible": "^1.1.1",
+    "vite-plugin-html": "^3.2.0",
+    "vite-plugin-importer": "^0.2.5",
+    "vite-plugin-style-import": "^1.4.1",
+    "vite-plugin-vue2": "^1.9.0",
+    "yarn-audit-fix": "^9.3.2"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
     },
-    "dependencies": {
-        "ant-design-vue": "^1.7.3",
-        "apexcharts": "^3.33.1",
-        "axios": "^0.21.2",
-        "brace": "https://github.com/nightwing/brace",
-        "chart.js": "^2.9.4",
-        "core-js": "^3.9.0",
-        "less": "^3.11.1",
-        "less-loader": "^5.0.0",
-        "lodash": "^4.17.21",
-        "lowlight": "^1.20.0",
-        "moment": "^2.24.0",
-        "node-sass": "^6.0.1",
-        "nprogress": "^0.2.0",
-        "reconnecting-websocket": "^4.4.0",
-        "vue": "^2.6.11",
-        "vue-apexcharts": "^1.6.2",
-        "vue-chartjs": "^3.5.1",
-        "vue-cli-plugin-generate-build-id": "^0.2.0",
-        "vue-codemirror": "^4.0.6",
-        "vue-cropper": "^0.5.8",
-        "vue-gettext": "^2.1.12",
-        "vue-itextarea": "^1.0.9",
-        "vue-router": "^3.5.1",
-        "vue-template-babel-compiler": "^1.1.3",
-        "vue-template-compiler": "^2.6.11",
-        "vue2-ace-editor": "^0.0.15",
-        "vuex": "^3.6.2",
-        "vuex-persist": "^3.1.3",
-        "xterm": "^4.19.0",
-        "xterm-addon-attach": "^0.6.0",
-        "xterm-addon-fit": "^0.5.0"
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
     },
-    "devDependencies": {
-        "@vue/cli-plugin-babel": "~4.5.15",
-        "@vue/cli-plugin-eslint": "~4.5.15",
-        "@vue/cli-plugin-router": "~4.5.15",
-        "@vue/cli-plugin-vuex": "~4.5.15",
-        "@vue/cli-service": "~4.5.0",
-        "babel-eslint": "^10.1.0",
-        "babel-plugin-import": "^1.13.3",
-        "easygettext": "^2.17.0",
-        "eslint": "^6.7.2",
-        "eslint-plugin-vue": "^6.2.2",
-        "yarn-audit-fix": "^9.3.2"
-    },
-    "eslintConfig": {
-        "root": true,
-        "env": {
-            "node": true
-        },
-        "extends": [
-            "plugin:vue/essential",
-            "eslint:recommended"
-        ],
-        "parserOptions": {
-            "parser": "babel-eslint"
-        },
-        "rules": {}
-    },
-    "postcss": {
-        "plugins": {
-            "autoprefixer": {}
-        }
-    },
-    "browserslist": [
-        "> 1%",
-        "last 2 versions",
-        "not dead"
-    ]
+    "rules": {}
+  },
+  "postcss": {
+    "plugins": {
+      "autoprefixer": {}
+    }
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
 }

+ 2 - 2
frontend/src/lazy.js

@@ -27,7 +27,7 @@ import {
     message,
     Modal,
     notification,
-    pageHeader,
+    PageHeader,
     Pagination,
     Popconfirm,
     Popover,
@@ -95,7 +95,7 @@ Vue.use(Transfer)
 Vue.use(Comment)
 Vue.use(Descriptions)
 Vue.use(Result)
-Vue.use(pageHeader)
+Vue.use(PageHeader)
 Vue.use(Switch)
 Vue.use(Space)
 Vue.use(Tag)

+ 1 - 1
frontend/src/lib/http/index.js

@@ -4,7 +4,7 @@ import {router} from '@/router'
 
 /* 创建 axios 实例 */
 let http = axios.create({
-    baseURL: process.env.VUE_APP_API_ROOT,
+    baseURL: import.meta.env.VITE_API_ROOT,
     timeout: 50000,
     headers: {'Content-Type': 'application/json'},
     transformRequest: [function (data, headers) {

+ 3 - 3
frontend/src/lib/utils/index.js

@@ -33,10 +33,10 @@ export default {
 
         Vue.prototype.getWebSocketRoot = () => {
             const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'
-            if (process.env.NODE_ENV === 'development' && process.env['VUE_APP_API_WSS_ROOT']) {
-                return process.env['VUE_APP_API_WSS_ROOT']
+            if (import.meta.env.MODE === 'development' && import.meta.env.VITE_API_WSS_ROOT) {
+                return import.meta.env.VITE_API_WSS_ROOT
             }
-            return protocol + location.host + process.env['VUE_APP_API_WSS_ROOT']
+            return protocol + location.host + import.meta.env.VITE_API_WSS_ROOT
         }
     }
 }

+ 16 - 17
frontend/src/locale/en/LC_MESSAGES/app.po

@@ -32,7 +32,7 @@ msgstr ""
 msgid "Add Location"
 msgstr ""
 
-#: src/router/index.js:47 src/views/domain/DomainAdd.vue:32
+#: src/router/index.js:47 src/views/domain/DomainAdd.vue:31
 msgid "Add Site"
 msgstr ""
 
@@ -64,7 +64,7 @@ msgstr ""
 msgid "Back"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:42
+#: src/views/domain/DomainAdd.vue:41
 msgid "Base information"
 msgstr ""
 
@@ -94,12 +94,12 @@ msgstr ""
 
 #: src/views/domain/ngx_conf/LocationEditor.vue:33
 #: src/views/domain/ngx_conf/LocationEditor.vue:77
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:65
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:67
 msgid "Comments"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:56
+#: src/views/domain/DomainAdd.vue:55
 msgid "Configuration Name"
 msgstr ""
 
@@ -107,7 +107,7 @@ msgstr ""
 msgid "Configurations"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:45
+#: src/views/domain/DomainAdd.vue:44
 msgid "Configure SSL"
 msgstr ""
 
@@ -124,7 +124,7 @@ msgstr ""
 msgid "CPU:"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:50 src/views/domain/DomainAdd.vue:5
+#: src/views/domain/DomainAdd.vue:46 src/views/domain/DomainAdd.vue:5
 msgid "Create Another"
 msgstr ""
 
@@ -181,7 +181,7 @@ msgstr ""
 msgid "Disk IO"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:135
+#: src/views/domain/DomainAdd.vue:125
 msgid "Domain Config Created Successfully"
 msgstr ""
 
@@ -213,7 +213,7 @@ msgstr ""
 msgid "Enable failed"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:94
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:20
 msgid "Enable TLS"
 msgstr ""
 
@@ -247,7 +247,7 @@ msgstr ""
 msgid "File Not Found"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:48
+#: src/views/domain/DomainAdd.vue:47
 msgid "Finished"
 msgstr ""
 
@@ -304,7 +304,7 @@ msgstr ""
 msgid "Logout successful"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:19
+#: src/views/domain/cert/IssueCert.vue:23
 msgid ""
 "Make sure you have configured a reverse proxy for .well-known\n"
 "            directory to HTTPChallengePort (default: 9180) before getting "
@@ -331,7 +331,7 @@ msgstr ""
 msgid "Memory and Storage"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainAdd.vue:2
+#: src/views/domain/DomainAdd.vue:43 src/views/domain/DomainAdd.vue:2
 msgid "Modify Config"
 msgstr ""
 
@@ -355,7 +355,7 @@ msgstr ""
 msgid "Network Total Send"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:40
+#: src/views/domain/DomainAdd.vue:36
 msgid "Next"
 msgstr ""
 
@@ -377,9 +377,8 @@ msgstr ""
 
 #: src/views/domain/cert/IssueCert.vue:15
 msgid ""
-"Note: The server_name in the current configuration must be the domain name "
-"you need to get the\n"
-"            certificate."
+"Note: The server_name in the current configuration must be the domain name\n"
+"            you need to get the certificate."
 msgstr ""
 
 #: src/router/index.js:137
@@ -515,7 +514,7 @@ msgstr ""
 msgid "Terminal"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/cert/IssueCert.vue:19
 msgid ""
 "The certificate for the domain will be checked every hour,\n"
 "            and will be renewed if it has been more than 1 month since it "
@@ -543,7 +542,7 @@ msgstr ""
 msgid "Username (*)"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:75 src/views/domain/cert/IssueCert.vue:49
+#: src/views/domain/DomainAdd.vue:74 src/views/domain/cert/IssueCert.vue:49
 msgid "Warning"
 msgstr ""
 

+ 16 - 17
frontend/src/locale/zh_CN/LC_MESSAGES/app.po

@@ -34,7 +34,7 @@ msgstr "在下面添加指令"
 msgid "Add Location"
 msgstr "添加 Location"
 
-#: src/router/index.js:47 src/views/domain/DomainAdd.vue:32
+#: src/router/index.js:47 src/views/domain/DomainAdd.vue:31
 msgid "Add Site"
 msgstr "添加站点"
 
@@ -66,7 +66,7 @@ msgstr "成功启用 %{name} 自动续签"
 msgid "Back"
 msgstr "返回"
 
-#: src/views/domain/DomainAdd.vue:42
+#: src/views/domain/DomainAdd.vue:41
 msgid "Base information"
 msgstr "基本信息"
 
@@ -96,12 +96,12 @@ msgstr "证书状态"
 
 #: src/views/domain/ngx_conf/LocationEditor.vue:33
 #: src/views/domain/ngx_conf/LocationEditor.vue:77
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:52
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:65
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:67
 msgid "Comments"
 msgstr "注释"
 
-#: src/views/domain/DomainAdd.vue:56
+#: src/views/domain/DomainAdd.vue:55
 msgid "Configuration Name"
 msgstr "配置名称"
 
@@ -109,7 +109,7 @@ msgstr "配置名称"
 msgid "Configurations"
 msgstr "配置"
 
-#: src/views/domain/DomainAdd.vue:45
+#: src/views/domain/DomainAdd.vue:44
 msgid "Configure SSL"
 msgstr "配置 SSL"
 
@@ -126,7 +126,7 @@ msgstr "CPU 状态"
 msgid "CPU:"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:50 src/views/domain/DomainAdd.vue:5
+#: src/views/domain/DomainAdd.vue:46 src/views/domain/DomainAdd.vue:5
 msgid "Create Another"
 msgstr "再创建一个"
 
@@ -183,7 +183,7 @@ msgstr "禁用成功"
 msgid "Disk IO"
 msgstr "磁盘 IO"
 
-#: src/views/domain/DomainAdd.vue:135
+#: src/views/domain/DomainAdd.vue:125
 msgid "Domain Config Created Successfully"
 msgstr "域名配置文件创建成功"
 
@@ -215,7 +215,7 @@ msgstr "启用 %{name} 自动续签失败"
 msgid "Enable failed"
 msgstr "启用失败"
 
-#: src/views/domain/DomainAdd.vue:94
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:20
 msgid "Enable TLS"
 msgstr "启用 TLS"
 
@@ -249,7 +249,7 @@ msgstr "启用失败 %{msg}"
 msgid "File Not Found"
 msgstr "未找到文件"
 
-#: src/views/domain/DomainAdd.vue:48
+#: src/views/domain/DomainAdd.vue:47
 msgid "Finished"
 msgstr "完成"
 
@@ -306,7 +306,7 @@ msgstr "登录成功"
 msgid "Logout successful"
 msgstr "登出成功"
 
-#: src/views/domain/cert/IssueCert.vue:19
+#: src/views/domain/cert/IssueCert.vue:23
 msgid ""
 "Make sure you have configured a reverse proxy for .well-known\n"
 "            directory to HTTPChallengePort (default: 9180) before getting "
@@ -335,7 +335,7 @@ msgstr "内存"
 msgid "Memory and Storage"
 msgstr "内存与存储"
 
-#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainAdd.vue:2
+#: src/views/domain/DomainAdd.vue:43 src/views/domain/DomainAdd.vue:2
 msgid "Modify Config"
 msgstr "修改配置文件"
 
@@ -359,7 +359,7 @@ msgstr "下载流量"
 msgid "Network Total Send"
 msgstr "上传流量"
 
-#: src/views/domain/DomainAdd.vue:40
+#: src/views/domain/DomainAdd.vue:36
 msgid "Next"
 msgstr "下一步"
 
@@ -381,9 +381,8 @@ msgstr "此前无效: %{date}"
 
 #: src/views/domain/cert/IssueCert.vue:15
 msgid ""
-"Note: The server_name in the current configuration must be the domain name "
-"you need to get the\n"
-"            certificate."
+"Note: The server_name in the current configuration must be the domain name\n"
+"            you need to get the certificate."
 msgstr "注意:当前配置中的 server_name 必须为需要申请证书的域名。"
 
 #: src/router/index.js:137
@@ -519,7 +518,7 @@ msgstr "系统消息"
 msgid "Terminal"
 msgstr "终端"
 
-#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/cert/IssueCert.vue:19
 msgid ""
 "The certificate for the domain will be checked every hour,\n"
 "            and will be renewed if it has been more than 1 month since it "
@@ -548,7 +547,7 @@ msgstr "用户名"
 msgid "Username (*)"
 msgstr "用户名 (*)"
 
-#: src/views/domain/DomainAdd.vue:75 src/views/domain/cert/IssueCert.vue:49
+#: src/views/domain/DomainAdd.vue:74 src/views/domain/cert/IssueCert.vue:49
 msgid "Warning"
 msgstr "警告"
 

Plik diff jest za duży
+ 0 - 0
frontend/src/translations.json


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

@@ -18,7 +18,7 @@
 
 <script>
 import StdTable from '@/components/StdDataDisplay/StdTable'
-import $gettext from "@/lib/translate/gettext";
+import $gettext from '@/lib/translate/gettext'
 
 const columns = [{
     title: $gettext('Name'),

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików