Browse Source

refactor: use new page templates

Shishkevich D. 13 hours ago
parent
commit
d642774a44
8 changed files with 762 additions and 759 deletions
  1. 0 31
      web/html/common/head.html
  2. 0 14
      web/html/common/js.html
  3. 58 0
      web/html/common/page.html
  4. 12 15
      web/html/inbounds.html
  5. 371 373
      web/html/index.html
  6. 168 170
      web/html/login.html
  7. 65 66
      web/html/settings.html
  8. 88 90
      web/html/xray.html

+ 0 - 31
web/html/common/head.html

@@ -1,31 +0,0 @@
-{{define "head"}}
-<head>
-  <meta charset="UTF-8">
-  <meta name="renderer" content="webkit">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <meta name="robots" content="noindex,nofollow">
-  <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
-  <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
-  <style>
-    [v-cloak] {
-      display: none;
-    }
-    /* vazirmatn-regular - arabic_latin_latin-ext */
-    @font-face {
-      font-display: swap;
-      font-family: 'Vazirmatn';
-      font-style: normal;
-      font-weight: 400;
-      src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
-      unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
-    }
-    body {
-      font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
-        'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
-        'Segoe UI Emoji', 'Segoe UI Symbol';
-    }
-  </style>
-  <title>{{ .host }} – {{ i18n .title}}</title>
-</head>
-<div id="message"></div>
-{{end}}

+ 0 - 14
web/html/common/js.html

@@ -1,14 +0,0 @@
-{{define "js"}}
-<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
-<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
-<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
-<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
-<script>
-    const basePath = '{{ .base_path }}';
-    axios.defaults.baseURL = basePath;
-</script>
-{{end}}

+ 58 - 0
web/html/common/page.html

@@ -0,0 +1,58 @@
+{{ define "page/head_start" }}
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta name="renderer" content="webkit">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="robots" content="noindex,nofollow">
+  <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
+  <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
+  <style>
+    [v-cloak] {
+      display: none;
+    }
+    /* vazirmatn-regular - arabic_latin_latin-ext */
+    @font-face {
+      font-display: swap;
+      font-family: 'Vazirmatn';
+      font-style: normal;
+      font-weight: 400;
+      src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
+      unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
+    }
+    body {
+      font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+    }
+  </style>
+  <title>{{ .host }} – {{ i18n .title}}</title>
+{{ end }}
+
+{{ define "page/head_end" }}
+</head>
+{{ end }}
+
+{{ define "page/body_start" }}
+<body>
+  <div id="message"></div>
+{{ end }}
+
+{{ define "page/body_scripts" }}
+<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
+<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
+<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
+<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
+<script>
+  const basePath = '{{ .base_path }}';
+  axios.defaults.baseURL = basePath;
+</script>
+{{ end }}
+  
+{{ define "page/body_end" }}
+</body>
+</html>
+{{ end }}

+ 12 - 15
web/html/inbounds.html

@@ -1,6 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-{{template "head" .}}
+{{ template "page/head_start" .}}
 <style>
   .ant-table:not(.ant-table-expanded-row .ant-table) {
     outline: 1px solid #f0f0f0;
@@ -143,8 +141,9 @@
     padding: 12px 2px;
   }
 </style>
+{{ template "page/head_end" .}}
 
-<body>
+{{ template "page/body_start" .}}
 <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
   <a-sidebar></a-sidebar>
   <a-layout id="content-layout">
@@ -657,7 +656,7 @@
     </a-layout-content>
   </a-layout>
 </a-layout>
-{{template "js" .}}
+{{template "page/body_scripts" .}}
 <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
@@ -666,6 +665,13 @@
 {{template "component/aThemeSwitch" .}}
 {{template "component/aCustomStatistic" .}}
 {{template "component/aPersianDatepicker" .}}
+{{template "modals/inboundModal"}}
+{{template "modals/promptModal"}}
+{{template "modals/qrcodeModal"}}
+{{template "modals/textModal"}}
+{{template "modals/inboundInfoModal"}}
+{{template "modals/clientsModal"}}
+{{template "modals/clientsBulkModal"}}
 <script>
     const columns = [{
         title: "ID",
@@ -1607,13 +1613,4 @@
         },
     });
 </script>
-
-{{template "modals/inboundModal"}}
-{{template "modals/promptModal"}}
-{{template "modals/qrcodeModal"}}
-{{template "modals/textModal"}}
-{{template "modals/inboundInfoModal"}}
-{{template "modals/clientsModal"}}
-{{template "modals/clientsBulkModal"}}
-</body>
-</html>
+{{ template "page/body_end" .}}

+ 371 - 373
web/html/index.html

@@ -1,6 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-{{template "head" .}}
+{{ template "page/head_start" .}}
 <style>
   @media (min-width: 769px) {
     .ant-layout-content {
@@ -79,384 +77,385 @@
     }
   }
 </style>
+{{ template "page/head_end" .}}
 
-<body>
-  <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
-    <a-sidebar></a-sidebar>
-    <a-layout id="content-layout">
-      <a-layout-content>
-        <a-spin :spinning="spinning" :delay="200" :tip="loadingTip">
-          <transition name="list" appear>
-            <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }"
-              message='{{ i18n "secAlertTitle" }}'
-              color="red"
-              description='{{ i18n "secAlertSsl" }}'
-              show-icon closable>
-            </a-alert>
-          </transition>
-          <transition name="list" appear>
-            <template>
-              <a-row v-if="!status.isLoaded">
-                <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
-                  <a-spin tip='{{ i18n "loading" }}'></a-spin>
+{{ template "page/body_start" .}}
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
+  <a-sidebar></a-sidebar>
+  <a-layout id="content-layout">
+    <a-layout-content>
+      <a-spin :spinning="spinning" :delay="200" :tip="loadingTip">
+        <transition name="list" appear>
+          <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }"
+            message='{{ i18n "secAlertTitle" }}'
+            color="red"
+            description='{{ i18n "secAlertSsl" }}'
+            show-icon closable>
+          </a-alert>
+        </transition>
+        <transition name="list" appear>
+          <template>
+            <a-row v-if="!status.isLoaded">
+              <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
+                <a-spin tip='{{ i18n "loading" }}'></a-spin>
+              </a-card>
+            </a-row>
+            <a-row v-else>
+              <a-row>
+                <a-card hoverable>
+                  <a-row>
+                    <a-col :sm="24" :md="12">
+                      <a-row>
+                        <a-col :span="12" :style="{ textAlign: 'center' }">
+                          <a-progress type="dashboard" status="normal"
+                            :stroke-color="status.cpu.color"
+                            :percent="status.cpu.percent"></a-progress>
+                          <div>
+                            <b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]] 
+                            <a-tooltip>
+                              <a-icon type="area-chart"></a-icon> 
+                              <template slot="title">
+                                <div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
+                                <div><b>{{ i18n "pages.index.frequency" }}:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
+                              </template>
+                            </a-tooltip>
+                          </div>
+                        </a-col>
+                        <a-col :span="12" :style="{ textAlign: 'center' }">
+                          <a-progress type="dashboard" status="normal"
+                            :stroke-color="status.mem.color"
+                            :percent="status.mem.percent"></a-progress>
+                          <div>
+                            <b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]]
+                          </div>
+                        </a-col>
+                      </a-row>
+                    </a-col>
+                    <a-col :sm="24" :md="12">
+                      <a-row>
+                        <a-col :span="12" :style="{ textAlign: 'center' }">
+                          <a-progress type="dashboard" status="normal"
+                            :stroke-color="status.swap.color"
+                            :percent="status.swap.percent"></a-progress>
+                          <div>
+                            <b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
+                          </div>
+                        </a-col>
+                        <a-col :span="12" :style="{ textAlign: 'center' }">
+                          <a-progress type="dashboard" status="normal"
+                            :stroke-color="status.disk.color"
+                            :percent="status.disk.percent"></a-progress>
+                          <div>
+                            <b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
+                          </div>
+                        </a-col>
+                      </a-row>
+                    </a-col>
+                  </a-row>
                 </a-card>
               </a-row>
-              <a-row v-else>
-                <a-row>
-                  <a-card hoverable>
-                    <a-row>
-                      <a-col :sm="24" :md="12">
-                        <a-row>
-                          <a-col :span="12" :style="{ textAlign: 'center' }">
-                            <a-progress type="dashboard" status="normal"
-                              :stroke-color="status.cpu.color"
-                              :percent="status.cpu.percent"></a-progress>
-                            <div>
-                              <b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]] 
-                              <a-tooltip>
-                                <a-icon type="area-chart"></a-icon> 
-                                <template slot="title">
-                                  <div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
-                                  <div><b>{{ i18n "pages.index.frequency" }}:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
-                                </template>
-                              </a-tooltip>
-                            </div>
-                          </a-col>
-                          <a-col :span="12" :style="{ textAlign: 'center' }">
-                            <a-progress type="dashboard" status="normal"
-                              :stroke-color="status.mem.color"
-                              :percent="status.mem.percent"></a-progress>
-                            <div>
-                              <b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]]
-                            </div>
-                          </a-col>
-                        </a-row>
-                      </a-col>
-                      <a-col :sm="24" :md="12">
-                        <a-row>
-                          <a-col :span="12" :style="{ textAlign: 'center' }">
-                            <a-progress type="dashboard" status="normal"
-                              :stroke-color="status.swap.color"
-                              :percent="status.swap.percent"></a-progress>
-                            <div>
-                              <b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
-                            </div>
-                          </a-col>
-                          <a-col :span="12" :style="{ textAlign: 'center' }">
-                            <a-progress type="dashboard" status="normal"
-                              :stroke-color="status.disk.color"
-                              :percent="status.disk.percent"></a-progress>
-                            <div>
-                              <b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
-                            </div>
-                          </a-col>
-                        </a-row>
-                      </a-col>
-                    </a-row>
-                  </a-card>
-                </a-row>
-                <a-col :sm="24" :lg="12">
-                  <a-card hoverable>
-                    <template #title>
-                      <a-space direction="horizontal">
-                        <span>{{ i18n "pages.index.xrayStatus" }}</span>
-                        <a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
-                          v[[ status.xray.version ]]
-                        </a-tag>
-                      </a-space>
-                    </template>
-                    <template #extra>
-                      <template v-if="status.xray.state != 'error'">
-                        <a-badge status="processing" class="running-animation" :text="status.xray.stateMsg" :color="status.xray.color"/>
-                      </template>
-                      <template v-else>
-                        <a-popover :overlay-class-name="themeSwitcher.currentTheme">
-                          <span slot="title">
-                            <a-row type="flex" align="middle" justify="space-between">
-                              <a-col>
-                                <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
-                              </a-col>
-                              <a-col>
-                                <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag>
-                              </a-col>
-                            </a-row>
-                          </span>
-                          <template slot="content">
-                            <span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
-                          </template>
-                          <a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
-                        </a-popover>
-                      </template>
+              <a-col :sm="24" :lg="12">
+                <a-card hoverable>
+                  <template #title>
+                    <a-space direction="horizontal">
+                      <span>{{ i18n "pages.index.xrayStatus" }}</span>
+                      <a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
+                        v[[ status.xray.version ]]
+                      </a-tag>
+                    </a-space>
+                  </template>
+                  <template #extra>
+                    <template v-if="status.xray.state != 'error'">
+                      <a-badge status="processing" class="running-animation" :text="status.xray.stateMsg" :color="status.xray.color"/>
                     </template>
-                    <template #actions>
-                      <a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
-                        <a-icon type="poweroff"></a-icon>
-                        <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
-                      </a-space>
-                      <a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
-                        <a-icon type="reload"></a-icon>
-                        <span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
-                      </a-space>
-                      <a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
-                        <a-icon type="tool"></a-icon>
-                        <span v-if="!isMobile">
-                          [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
+                    <template v-else>
+                      <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                        <span slot="title">
+                          <a-row type="flex" align="middle" justify="space-between">
+                            <a-col>
+                              <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
+                            </a-col>
+                            <a-col>
+                              <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag>
+                            </a-col>
+                          </a-row>
                         </span>
-                      </a-space>
-                    </template>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "menu.link" }}' hoverable>
-                    <template #actions>
-                      <a-space direction="horizontal" @click="openLogs()" :style="{ justifyContent: 'center' }">
-                        <a-icon type="bars"></a-icon>
-                        <span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
-                      </a-space>
-                      <a-space direction="horizontal" @click="openConfig" :style="{ justifyContent: 'center' }">
-                        <a-icon type="control"></a-icon>
-                        <span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
-                      </a-space>
-                      <a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
-                        <a-icon type="cloud-server"></a-icon>
-                        <span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
-                      </a-space>
+                        <template slot="content">
+                          <span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
+                        </template>
+                        <a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
+                      </a-popover>
                     </template>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='3X-UI' hoverable>
-                    <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank">
-                      <a-tag color="green">
-                        <span>v{{ .cur_ver }}</span>
-                      </a-tag>
-                    </a>
-                    <a rel="noopener" href="https://t.me/XrayUI" target="_blank">
-                      <a-tag color="green">
-                        <span>@XrayUI</span>
-                      </a-tag>
-                    </a>
-                    <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank">
-                      <a-tag>
-                        <span>{{ i18n "pages.index.documentation" }}</span>
-                      </a-tag>
-                    </a>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
-                    <a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag>
-                    <a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
+                  </template>
+                  <template #actions>
+                    <a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
+                      <a-icon type="poweroff"></a-icon>
+                      <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
+                    </a-space>
+                    <a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
+                      <a-icon type="reload"></a-icon>
+                      <span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
+                    </a-space>
+                    <a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
+                      <a-icon type="tool"></a-icon>
+                      <span v-if="!isMobile">
+                        [[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
+                      </span>
+                    </a-space>
+                  </template>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "menu.link" }}' hoverable>
+                  <template #actions>
+                    <a-space direction="horizontal" @click="openLogs()" :style="{ justifyContent: 'center' }">
+                      <a-icon type="bars"></a-icon>
+                      <span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
+                    </a-space>
+                    <a-space direction="horizontal" @click="openConfig" :style="{ justifyContent: 'center' }">
+                      <a-icon type="control"></a-icon>
+                      <span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
+                    </a-space>
+                    <a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
+                      <a-icon type="cloud-server"></a-icon>
+                      <span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
+                    </a-space>
+                  </template>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='3X-UI' hoverable>
+                  <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank">
                     <a-tag color="green">
-                      <a-tooltip>
-                        [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
-                        <template slot="title">
-                          {{ i18n "pages.index.systemLoadDesc" }}
-                        </template>
-                      </a-tooltip>
+                      <span>v{{ .cur_ver }}</span>
+                    </a-tag>
+                  </a>
+                  <a rel="noopener" href="https://t.me/XrayUI" target="_blank">
+                    <a-tag color="green">
+                      <span>@XrayUI</span>
                     </a-tag>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "usage"}}' hoverable>
-                    <a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
-                    <a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
-                    <a-row :gutter="isMobile ? [8,8] : 0">
-                      <a-col :span="12">
-                        <a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)">
-                          <template #prefix>
-                            <a-icon type="arrow-up" />
-                          </template>
-                          <template #suffix>
-                            /s
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                      <a-col :span="12">
-                        <a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)">
-                          <template #prefix>
-                            <a-icon type="arrow-down" />
-                          </template>
-                          <template #suffix>
-                            /s
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                    </a-row>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
-                    <a-row :gutter="isMobile ? [8,8] : 0">
-                      <a-col :span="12">
-                        <a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
-                          <template #prefix>
-                            <a-icon type="cloud-upload" />
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                      <a-col :span="12">
-                        <a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
-                          <template #prefix>
-                            <a-icon type="cloud-download" />
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                    </a-row>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
-                    <template #extra>
-                      <a-tooltip :placement="isMobile ? 'topRight' : 'top'">
-                        <template #title>
-                          {{ i18n "pages.index.toggleIpVisibility" }}
+                  </a>
+                  <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank">
+                    <a-tag color="purple">
+                      <span>{{ i18n "pages.index.documentation" }}</span>
+                    </a-tag>
+                  </a>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
+                  <a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag>
+                  <a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
+                  <a-tag color="green">
+                    <a-tooltip>
+                      [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
+                      <template slot="title">
+                        {{ i18n "pages.index.systemLoadDesc" }}
+                      </template>
+                    </a-tooltip>
+                  </a-tag>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "usage"}}' hoverable>
+                  <a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
+                  <a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
+                  <a-row :gutter="isMobile ? [8,8] : 0">
+                    <a-col :span="12">
+                      <a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)">
+                        <template #prefix>
+                          <a-icon type="arrow-up" />
                         </template>
-                        <a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
-                      </a-tooltip>
-                    </template>
-                    <a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
-                      <a-col :span="isMobile ? 24 : 12">
-                        <a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
-                          <template #prefix>
-                            <a-icon type="global" />
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                      <a-col :span="isMobile ? 24 : 12">
-                        <a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
-                          <template #prefix>
-                            <a-icon type="global" />
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                    </a-row>
-                  </a-card>
-                </a-col>
-                <a-col :sm="24" :lg="12">
-                  <a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
-                    <a-row :gutter="isMobile ? [8,8] : 0">
-                      <a-col :span="12">
-                        <a-custom-statistic title="TCP" :value="status.tcpCount">
-                          <template #prefix>
-                            <a-icon type="swap" />
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                      <a-col :span="12">
-                        <a-custom-statistic title="UDP" :value="status.udpCount">
-                          <template #prefix>
-                            <a-icon type="swap" />
-                          </template>
-                        </a-custom-statistic>
-                      </a-col>
-                    </a-row>
-                  </a-card>
-                </a-col>
-              </a-row>
-            </template>
-          </transition>
-        </a-spin>
-      </a-layout-content>
-    </a-layout>
-    <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
-        @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
-      <a-collapse default-active-key="1">
-        <a-collapse-panel key="1" header='Xray'>
-          <a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
-          <a-list class="ant-version-list" bordered :style="{ width: '100%' }">
-            <a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
-              <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
-              <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
-            </a-list-item>
-          </a-list>
-        </a-collapse-panel>
-        <a-collapse-panel key="2" header='Geofiles'>
-          <a-list class="ant-version-list" bordered :style="{ width: '100%' }">
-            <a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
-              <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
-              <a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
-            </a-list-item>
-          </a-list>
-        </a-collapse-panel>
-      </a-collapse>
-    </a-modal>
-    <a-modal id="log-modal" v-model="logModal.visible"
-        :closable="true" @cancel="() => logModal.visible = false"
-        :class="themeSwitcher.currentTheme"
-        width="800px" footer="">
-      <template slot="title">
-        {{ i18n "pages.index.logs" }}
-        <a-icon :spin="logModal.loading"
-          type="sync"
-          :style="{ verticalAlign: 'middle', marginLeft: '10px' }"
-          :disabled="logModal.loading"
-          @click="openLogs()">
-        </a-icon>
-      </template>
-      <a-form layout="inline">
-        <a-form-item :style="{ marginRight: '0.5rem' }">
-          <a-input-group compact>
-            <a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
-                @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
-              <a-select-option value="10">10</a-select-option>
-              <a-select-option value="20">20</a-select-option>
-              <a-select-option value="50">50</a-select-option>
-              <a-select-option value="100">100</a-select-option>
-              <a-select-option value="500">500</a-select-option>
-            </a-select>
-            <a-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
-                @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
-              <a-select-option value="debug">Debug</a-select-option>
-              <a-select-option value="info">Info</a-select-option>
-              <a-select-option value="notice">Notice</a-select-option>
-              <a-select-option value="warning">Warning</a-select-option>
-              <a-select-option value="err">Error</a-select-option>
-            </a-select>
-          </a-input-group>
-        </a-form-item>
-        <a-form-item>
-          <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
-        </a-form-item>
-        <a-form-item :style="{ float: 'right' }">
-          <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
-        </a-form-item>
-      </a-form>
-      <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
-    </a-modal>
-    <a-modal id="backup-modal" 
-        v-model="backupModal.visible" 
-        title='{{ i18n "pages.index.backupTitle" }}'
-        :closable="true"
-        footer=""
-        :class="themeSwitcher.currentTheme">
-      <a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
-        <a-list-item class="ant-backup-list-item">
-          <a-list-item-meta>
-            <template #title>{{ i18n "pages.index.exportDatabase" }}</template>
-            <template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
-          </a-list-item-meta>
-          <a-button @click="exportDatabase()" type="primary" icon="download"/>
-        </a-list-item>
-        <a-list-item class="ant-backup-list-item">
-          <a-list-item-meta>
-            <template #title>{{ i18n "pages.index.importDatabase" }}</template>
-            <template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
-          </a-list-item-meta>
-          <a-button @click="importDatabase()" type="primary" icon="upload" />
-        </a-list-item>
-      </a-list>
-    </a-modal>
+                        <template #suffix>
+                          /s
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                    <a-col :span="12">
+                      <a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)">
+                        <template #prefix>
+                          <a-icon type="arrow-down" />
+                        </template>
+                        <template #suffix>
+                          /s
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                  </a-row>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
+                  <a-row :gutter="isMobile ? [8,8] : 0">
+                    <a-col :span="12">
+                      <a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
+                        <template #prefix>
+                          <a-icon type="cloud-upload" />
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                    <a-col :span="12">
+                      <a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
+                        <template #prefix>
+                          <a-icon type="cloud-download" />
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                  </a-row>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
+                  <template #extra>
+                    <a-tooltip :placement="isMobile ? 'topRight' : 'top'">
+                      <template #title>
+                        {{ i18n "pages.index.toggleIpVisibility" }}
+                      </template>
+                      <a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
+                    </a-tooltip>
+                  </template>
+                  <a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
+                    <a-col :span="isMobile ? 24 : 12">
+                      <a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
+                        <template #prefix>
+                          <a-icon type="global" />
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                    <a-col :span="isMobile ? 24 : 12">
+                      <a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
+                        <template #prefix>
+                          <a-icon type="global" />
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                  </a-row>
+                </a-card>
+              </a-col>
+              <a-col :sm="24" :lg="12">
+                <a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
+                  <a-row :gutter="isMobile ? [8,8] : 0">
+                    <a-col :span="12">
+                      <a-custom-statistic title="TCP" :value="status.tcpCount">
+                        <template #prefix>
+                          <a-icon type="swap" />
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                    <a-col :span="12">
+                      <a-custom-statistic title="UDP" :value="status.udpCount">
+                        <template #prefix>
+                          <a-icon type="swap" />
+                        </template>
+                      </a-custom-statistic>
+                    </a-col>
+                  </a-row>
+                </a-card>
+              </a-col>
+            </a-row>
+          </template>
+        </transition>
+      </a-spin>
+    </a-layout-content>
   </a-layout>
-{{template "js" .}}
+  <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
+      @ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
+    <a-collapse default-active-key="1">
+      <a-collapse-panel key="1" header='Xray'>
+        <a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
+        <a-list class="ant-version-list" bordered :style="{ width: '100%' }">
+          <a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
+            <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
+            <a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
+          </a-list-item>
+        </a-list>
+      </a-collapse-panel>
+      <a-collapse-panel key="2" header='Geofiles'>
+        <a-list class="ant-version-list" bordered :style="{ width: '100%' }">
+          <a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
+            <a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
+            <a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
+          </a-list-item>
+        </a-list>
+      </a-collapse-panel>
+    </a-collapse>
+  </a-modal>
+  <a-modal id="log-modal" v-model="logModal.visible"
+      :closable="true" @cancel="() => logModal.visible = false"
+      :class="themeSwitcher.currentTheme"
+      width="800px" footer="">
+    <template slot="title">
+      {{ i18n "pages.index.logs" }}
+      <a-icon :spin="logModal.loading"
+        type="sync"
+        :style="{ verticalAlign: 'middle', marginLeft: '10px' }"
+        :disabled="logModal.loading"
+        @click="openLogs()">
+      </a-icon>
+    </template>
+    <a-form layout="inline">
+      <a-form-item :style="{ marginRight: '0.5rem' }">
+        <a-input-group compact>
+          <a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
+              @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
+            <a-select-option value="10">10</a-select-option>
+            <a-select-option value="20">20</a-select-option>
+            <a-select-option value="50">50</a-select-option>
+            <a-select-option value="100">100</a-select-option>
+            <a-select-option value="500">500</a-select-option>
+          </a-select>
+          <a-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
+              @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
+            <a-select-option value="debug">Debug</a-select-option>
+            <a-select-option value="info">Info</a-select-option>
+            <a-select-option value="notice">Notice</a-select-option>
+            <a-select-option value="warning">Warning</a-select-option>
+            <a-select-option value="err">Error</a-select-option>
+          </a-select>
+        </a-input-group>
+      </a-form-item>
+      <a-form-item>
+        <a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
+      </a-form-item>
+      <a-form-item :style="{ float: 'right' }">
+        <a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
+      </a-form-item>
+    </a-form>
+    <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
+  </a-modal>
+  <a-modal id="backup-modal" 
+      v-model="backupModal.visible" 
+      title='{{ i18n "pages.index.backupTitle" }}'
+      :closable="true"
+      footer=""
+      :class="themeSwitcher.currentTheme">
+    <a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
+      <a-list-item class="ant-backup-list-item">
+        <a-list-item-meta>
+          <template #title>{{ i18n "pages.index.exportDatabase" }}</template>
+          <template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
+        </a-list-item-meta>
+        <a-button @click="exportDatabase()" type="primary" icon="download"/>
+      </a-list-item>
+      <a-list-item class="ant-backup-list-item">
+        <a-list-item-meta>
+          <template #title>{{ i18n "pages.index.importDatabase" }}</template>
+          <template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
+        </a-list-item-meta>
+        <a-button @click="importDatabase()" type="primary" icon="upload" />
+      </a-list-item>
+    </a-list>
+  </a-modal>
+</a-layout>
+{{template "page/body_scripts" .}}
 {{template "component/aSidebar" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aCustomStatistic" .}}
@@ -788,5 +787,4 @@
         },
     });
 </script>
-</body>
-</html>
+{{ template "page/body_end" .}}

+ 168 - 170
web/html/login.html

@@ -1,6 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-{{template "head" .}}
+{{ template "page/head_start" .}}
 <style>
   html * {
     -webkit-font-smoothing: antialiased;
@@ -453,174 +451,174 @@
     margin: 2px 0 4px;
   }
 </style>
-
-<body>
-  <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
-    <transition name="list" appear>
-      <a-layout-content class="under" :style="{ minHeight: '0' }">
-        <div class="waves-header">
-          <div class="waves-inner-header"></div>
-          <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
-            viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
-            <defs>
-              <path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
-            </defs>
-            <g class="parallax">
-              <use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
-              <use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
-              <use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
-              <use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
-            </g>
-          </svg>
-        </div>
-        <a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
-          <a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
-            <div class="setting-section">
-              <a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
-                <template slot="content">
-                  <a-space direction="vertical" :size="10">
-                    <a-theme-switch-login></a-theme-switch-login>
-                    <span>{{ i18n "pages.settings.language" }}</span>
-                    <a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
-                      <a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
-                        <span role="img" aria-label="l.name" v-text="l.icon"></span>
-                        &nbsp;&nbsp;<span v-text="l.name"></span>
-                      </a-select-option>
-                    </a-select>
-                  </a-space>
-                </template>
-                <a-button shape="circle" icon="setting"></a-button>
-              </a-popover>
-            </div>
-            <a-row type="flex" justify="center">
-              <a-col :style="{ width: '100%' }">
-                <h2 class="title headline zoom">
-                  <span class="words-wrapper">
-                    <b class="is-visible">{{ i18n "pages.login.hello" }}</b>
-                    <b>{{ i18n "pages.login.title" }}</b>
-                  </span>
-                </h2>
-              </a-col>
-            </a-row>
-            <a-row type="flex" justify="center">
-              <a-col span="24">
-                <a-form>
-                  <a-space direction="vertical" size="middle">
-                    <a-form-item>
-                      <a-input autocomplete="username" name="username" v-model.trim="user.username"
-                        placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
-                        <a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
-                      </a-input>
-                    </a-form-item>
-                    <a-form-item>
-                      <a-input-password autocomplete="password" name="password" v-model.trim="user.password"
-                        placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
-                        <a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
-                      </a-input-password>
-                    </a-form-item>
-                    <a-form-item v-if="twoFactorEnable">
-                      <a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
-                        placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
-                        <a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
-                      </a-input>
-                    </a-form-item>
-                    <a-form-item>
-                      <a-row justify="center" class="centered">
-                        <div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
-                          <a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
-                            :icon="loading ? 'poweroff' : undefined">
-                            [[ loading ? '' : '{{ i18n "login" }}' ]]
-                          </a-button>
-                        </div>
-                      </a-row>
-                    </a-form-item>
-                  </a-space>
-                </a-form>
-              </a-col>
-            </a-row>
-          </a-col>
-        </a-row>
-      </a-layout-content>
-    </transition>
-  </a-layout>
-  {{template "js" .}}
-  {{template "component/aThemeSwitch" .}}
-  <script>
-    const app = new Vue({
-      delimiters: ['[[', ']]'],
-      el: '#app',
-      data: {
-        themeSwitcher,
-        loading: false,
-        user: {
-          username: "",
-          password: "",
-          twoFactorCode: ""
-        },
-        twoFactorEnable: false,
-        lang: ""
+{{ template "page/head_end" .}}
+
+{{ template "page/body_start" .}}
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
+  <transition name="list" appear>
+    <a-layout-content class="under" :style="{ minHeight: '0' }">
+      <div class="waves-header">
+        <div class="waves-inner-header"></div>
+        <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+          viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
+          <defs>
+            <path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
+          </defs>
+          <g class="parallax">
+            <use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
+            <use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
+            <use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
+            <use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
+          </g>
+        </svg>
+      </div>
+      <a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
+        <a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
+          <div class="setting-section">
+            <a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
+              <template slot="content">
+                <a-space direction="vertical" :size="10">
+                  <a-theme-switch-login></a-theme-switch-login>
+                  <span>{{ i18n "pages.settings.language" }}</span>
+                  <a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
+                    <a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
+                      <span role="img" aria-label="l.name" v-text="l.icon"></span>
+                      &nbsp;&nbsp;<span v-text="l.name"></span>
+                    </a-select-option>
+                  </a-select>
+                </a-space>
+              </template>
+              <a-button shape="circle" icon="setting"></a-button>
+            </a-popover>
+          </div>
+          <a-row type="flex" justify="center">
+            <a-col :style="{ width: '100%' }">
+              <h2 class="title headline zoom">
+                <span class="words-wrapper">
+                  <b class="is-visible">{{ i18n "pages.login.hello" }}</b>
+                  <b>{{ i18n "pages.login.title" }}</b>
+                </span>
+              </h2>
+            </a-col>
+          </a-row>
+          <a-row type="flex" justify="center">
+            <a-col span="24">
+              <a-form>
+                <a-space direction="vertical" size="middle">
+                  <a-form-item>
+                    <a-input autocomplete="username" name="username" v-model.trim="user.username"
+                      placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
+                      <a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
+                    </a-input>
+                  </a-form-item>
+                  <a-form-item>
+                    <a-input-password autocomplete="password" name="password" v-model.trim="user.password"
+                      placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
+                      <a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
+                    </a-input-password>
+                  </a-form-item>
+                  <a-form-item v-if="twoFactorEnable">
+                    <a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
+                      placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
+                      <a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
+                    </a-input>
+                  </a-form-item>
+                  <a-form-item>
+                    <a-row justify="center" class="centered">
+                      <div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
+                        <a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
+                          :icon="loading ? 'poweroff' : undefined">
+                          [[ loading ? '' : '{{ i18n "login" }}' ]]
+                        </a-button>
+                      </div>
+                    </a-row>
+                  </a-form-item>
+                </a-space>
+              </a-form>
+            </a-col>
+          </a-row>
+        </a-col>
+      </a-row>
+    </a-layout-content>
+  </transition>
+</a-layout>
+{{template "page/body_scripts" .}}
+{{template "component/aThemeSwitch" .}}
+<script>
+  const app = new Vue({
+    delimiters: ['[[', ']]'],
+    el: '#app',
+    data: {
+      themeSwitcher,
+      loading: false,
+      user: {
+        username: "",
+        password: "",
+        twoFactorCode: ""
       },
-      async mounted() {
-        this.lang = LanguageManager.getLanguage();
-        this.twoFactorEnable = await this.getTwoFactorEnable();
+      twoFactorEnable: false,
+      lang: ""
+    },
+    async mounted() {
+      this.lang = LanguageManager.getLanguage();
+      this.twoFactorEnable = await this.getTwoFactorEnable();
+    },
+    methods: {
+      async login() {
+        this.loading = true;
+        const msg = await HttpUtil.post('/login', this.user);
+        this.loading = false;
+        if (msg.success) {
+          location.href = basePath + 'panel/';
+        }
       },
-      methods: {
-        async login() {
-          this.loading = true;
-          const msg = await HttpUtil.post('/login', this.user);
-          this.loading = false;
-          if (msg.success) {
-            location.href = basePath + 'panel/';
-          }
-        },
-        async getTwoFactorEnable() {
-          this.loading = true;
-          const msg = await HttpUtil.post('/getTwoFactorEnable');
-          this.loading = false;
-          if (msg.success) {
-            this.twoFactorEnable = msg.obj;
-            return msg.obj;
-          }
-        },
+      async getTwoFactorEnable() {
+        this.loading = true;
+        const msg = await HttpUtil.post('/getTwoFactorEnable');
+        this.loading = false;
+        if (msg.success) {
+          this.twoFactorEnable = msg.obj;
+          return msg.obj;
+        }
       },
-    });
-
-    document.addEventListener("DOMContentLoaded", function () {
-      var animationDelay = 2000;
-      initHeadline();
-
-      function initHeadline() {
-        animateHeadline(document.querySelectorAll('.headline'));
-      }
-
-      function animateHeadline(headlines) {
-        var duration = animationDelay;
-        headlines.forEach(function (headline) {
-          setTimeout(function () {
-            hideWord(headline.querySelector('.is-visible'));
-          }, duration);
-        });
-      }
-
-      function hideWord(word) {
-        var nextWord = takeNext(word);
-        switchWord(word, nextWord);
+    },
+  });
+
+  document.addEventListener("DOMContentLoaded", function () {
+    var animationDelay = 2000;
+    initHeadline();
+
+    function initHeadline() {
+      animateHeadline(document.querySelectorAll('.headline'));
+    }
+
+    function animateHeadline(headlines) {
+      var duration = animationDelay;
+      headlines.forEach(function (headline) {
         setTimeout(function () {
-          hideWord(nextWord);
-        }, animationDelay);
-      }
-
-      function takeNext(word) {
-        return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
-      }
-
-      function switchWord(oldWord, newWord) {
-        oldWord.classList.remove('is-visible');
-        oldWord.classList.add('is-hidden');
-        newWord.classList.remove('is-hidden');
-        newWord.classList.add('is-visible');
-      }
-    });
-  </script>
-</body>
-</html>
+          hideWord(headline.querySelector('.is-visible'));
+        }, duration);
+      });
+    }
+
+    function hideWord(word) {
+      var nextWord = takeNext(word);
+      switchWord(word, nextWord);
+      setTimeout(function () {
+        hideWord(nextWord);
+      }, animationDelay);
+    }
+
+    function takeNext(word) {
+      return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
+    }
+
+    function switchWord(oldWord, newWord) {
+      oldWord.classList.remove('is-visible');
+      oldWord.classList.add('is-hidden');
+      newWord.classList.remove('is-hidden');
+      newWord.classList.add('is-visible');
+    }
+  });
+</script>
+{{ template "page/body_end" .}}

+ 65 - 66
web/html/settings.html

@@ -1,6 +1,4 @@
-<!DOCTYPE html>
-<html lang="en">
-{{template "head" .}}
+{{ template "page/head_start" .}}
 <style>
   @media (min-width: 769px) {
     .ant-layout-content {
@@ -60,68 +58,70 @@
     margin-block-end: 12px;
   }
 </style>
-<body>
-  <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
-    <a-sidebar></a-sidebar>
-    <a-layout id="content-layout">
-      <a-layout-content>
-        <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
-          <transition name="list" appear>
-            <a-alert type="error" v-if="confAlerts.length>0" :style="{ marginBottom: '10px' }"
-                message='{{ i18n "secAlertTitle" }}'
-                color="red"
-                show-icon closable>
-              <template slot="description">
-                <b>{{ i18n "secAlertConf" }}</b>
-                <ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
-              </template>
-            </a-alert>
-          </transition>
-          <a-space direction="vertical">
-            <a-card hoverable :style="{ marginBottom: '.5rem', overflowX: 'hidden' }">
-              <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
-                <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
-                  <a-space direction="horizontal">
-                    <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
-                    <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
-                  </a-space>
-                </a-col>
-                <a-col :xs="24" :sm="14">
-                  <template>
-                    <div>
-                      <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
-                      <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
-                        message='{{ i18n "pages.settings.infoDesc" }}'
-                        show-icon>
-                      </a-alert>
-                    </div>
-                  </template>
-                </a-col>
-              </a-row>
-            </a-card>
-            <a-tabs default-active-key="1">
-              <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings" }}' :style="{ paddingTop: '20px' }">
-                {{ template "settings/panel/general" . }}
-              </a-tab-pane>
-              <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings" }}' :style="{ paddingTop: '20px' }">
-                {{ template "settings/panel/security" . }}
-              </a-tab-pane>
-              <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings" }}' :style="{ paddingTop: '20px' }">
-                {{ template "settings/panel/telegram" . }}
-              </a-tab-pane>
-              <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}' :style="{ paddingTop: '20px' }">
-                {{ template "settings/panel/subscription/general" . }}
-              </a-tab-pane>
-              <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
-                {{ template "settings/panel/subscription/json" . }}
-              </a-tab-pane>
-            </a-tabs>
-          </a-space>
-        </a-spin>
-      </a-layout-content>
-    </a-layout>
+{{ template "page/head_end" .}}
+
+{{ template "page/body_start" .}}
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
+  <a-sidebar></a-sidebar>
+  <a-layout id="content-layout">
+    <a-layout-content>
+      <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
+        <transition name="list" appear>
+          <a-alert type="error" v-if="confAlerts.length>0" :style="{ marginBottom: '10px' }"
+              message='{{ i18n "secAlertTitle" }}'
+              color="red"
+              show-icon closable>
+            <template slot="description">
+              <b>{{ i18n "secAlertConf" }}</b>
+              <ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
+            </template>
+          </a-alert>
+        </transition>
+        <a-space direction="vertical">
+          <a-card hoverable :style="{ marginBottom: '.5rem', overflowX: 'hidden' }">
+            <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
+              <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
+                <a-space direction="horizontal">
+                  <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
+                  <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
+                </a-space>
+              </a-col>
+              <a-col :xs="24" :sm="14">
+                <template>
+                  <div>
+                    <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
+                    <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
+                      message='{{ i18n "pages.settings.infoDesc" }}'
+                      show-icon>
+                    </a-alert>
+                  </div>
+                </template>
+              </a-col>
+            </a-row>
+          </a-card>
+          <a-tabs default-active-key="1">
+            <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings" }}' :style="{ paddingTop: '20px' }">
+              {{ template "settings/panel/general" . }}
+            </a-tab-pane>
+            <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings" }}' :style="{ paddingTop: '20px' }">
+              {{ template "settings/panel/security" . }}
+            </a-tab-pane>
+            <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings" }}' :style="{ paddingTop: '20px' }">
+              {{ template "settings/panel/telegram" . }}
+            </a-tab-pane>
+            <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}' :style="{ paddingTop: '20px' }">
+              {{ template "settings/panel/subscription/general" . }}
+            </a-tab-pane>
+            <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
+              {{ template "settings/panel/subscription/json" . }}
+            </a-tab-pane>
+          </a-tabs>
+        </a-space>
+      </a-spin>
+    </a-layout-content>
   </a-layout>
-{{template "js" .}}
+</a-layout>
+{{template "page/body_scripts" .}}
 <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
@@ -531,5 +531,4 @@
     }
   });
 </script>
-</body>
-</html>
+{{ template "page/body_end" .}}

+ 88 - 90
web/html/xray.html

@@ -1,22 +1,8 @@
-<!DOCTYPE html>
-<html lang="en">
-{{template "head" .}}
+{{ template "page/head_start" .}}
 <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
 <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
 <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
 <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
-
-<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
-<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
-<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
-<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
-<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
-<script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
-<script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
-<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
-<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
-<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
 <style>
   @media (min-width: 769px) {
     .ant-layout-content {
@@ -46,79 +32,92 @@
     margin-block-end: 12px;
 }
 </style>
-<body>
-  <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
-    <a-sidebar></a-sidebar>
-    <a-layout id="content-layout">
-      <a-layout-content>
-        <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
-          <transition name="list" appear>
-            <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }"
-              message='{{ i18n "secAlertTitle" }}'
-              color="red"
-              description='{{ i18n "secAlertSsl" }}'
-              show-icon closable>
-            </a-alert>
-          </transition>
-          <a-space direction="vertical">
-            <a-card hoverable :style="{ marginBottom: '.5rem' }">
-              <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
-                <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
-                  <a-space direction="horizontal">
-                    <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.xray.save" }}</a-button>
-                    <a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">{{ i18n "pages.xray.restart" }}</a-button>
-                    <a-popover v-if="restartResult"
-                        :overlay-class-name="themeSwitcher.currentTheme">
-                      <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
-                      <template slot="content">
-                        <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span>
-                      </template>
-                      <a-icon type="question-circle"></a-icon>
-                    </a-popover>
-                  </a-space>
-                </a-col>
-                <a-col :xs="24" :sm="14">
-                  <template>
-                    <div>
-                      <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
-                      <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }" message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
-                      </a-alert>
-                    </div>
-                  </template>
-                </a-col>
-              </a-row>
-            </a-card>
-            <a-tabs class="ant-card-dark-box-nohover" default-active-key="1"
-                @change="(activeKey) => { this.changePage(activeKey); }"
-                :class="themeSwitcher.currentTheme">
-              <a-tab-pane key="tpl-basic" tab='{{ i18n "pages.xray.basicTemplate"}}' :style="{ paddingTop: '20px' }">
-                {{ template "settings/xray/basics" . }}
-              </a-tab-pane>
-              <a-tab-pane key="tpl-routing" tab='{{ i18n "pages.xray.Routings"}}' :style="{ paddingTop: '20px' }">
-                {{ template "settings/xray/routing" . }}
-              </a-tab-pane>
-              <a-tab-pane key="tpl-outbound" tab='{{ i18n "pages.xray.Outbounds"}}' force-render="true">
-                {{ template "settings/xray/outbounds" . }}
-              </a-tab-pane>
-              <a-tab-pane key="tpl-reverse" tab='{{ i18n "pages.xray.outbound.reverse"}}' :style="{ paddingTop: '20px' }" force-render="true">
-                {{ template "settings/xray/reverse" . }}
-              </a-tab-pane>
-              <a-tab-pane key="tpl-balancer" tab='{{ i18n "pages.xray.Balancers"}}' :style="{ paddingTop: '20px' }" force-render="true">
-                {{ template "settings/xray/balancers" . }}
-              </a-tab-pane>
-              <a-tab-pane key="tpl-dns" tab='DNS' :style="{ paddingTop: '20px' }" force-render="true">
-                {{ template "settings/xray/dns" . }}
-              </a-tab-pane>
-              <a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' :style="{ paddingTop: '20px' }" force-render="true">
-                {{ template "settings/xray/advanced" . }}
-              </a-tab-pane>
-            </a-tabs>
-          </a-space>
-        </a-spin>
-      </a-layout-content>
-    </a-layout>
-  </a-layout>
-{{template "js" .}}
+{{ template "page/head_end" .}}
+
+{{ template "page/body_start" .}}
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
+<a-sidebar></a-sidebar>
+<a-layout id="content-layout">
+    <a-layout-content>
+    <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
+        <transition name="list" appear>
+        <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }"
+            message='{{ i18n "secAlertTitle" }}'
+            color="red"
+            description='{{ i18n "secAlertSsl" }}'
+            show-icon closable>
+        </a-alert>
+        </transition>
+        <a-space direction="vertical">
+        <a-card hoverable :style="{ marginBottom: '.5rem' }">
+            <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
+            <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
+                <a-space direction="horizontal">
+                <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.xray.save" }}</a-button>
+                <a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">{{ i18n "pages.xray.restart" }}</a-button>
+                <a-popover v-if="restartResult"
+                    :overlay-class-name="themeSwitcher.currentTheme">
+                    <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
+                    <template slot="content">
+                    <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span>
+                    </template>
+                    <a-icon type="question-circle"></a-icon>
+                </a-popover>
+                </a-space>
+            </a-col>
+            <a-col :xs="24" :sm="14">
+                <template>
+                <div>
+                    <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
+                    <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }" message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
+                    </a-alert>
+                </div>
+                </template>
+            </a-col>
+            </a-row>
+        </a-card>
+        <a-tabs class="ant-card-dark-box-nohover" default-active-key="1"
+            @change="(activeKey) => { this.changePage(activeKey); }"
+            :class="themeSwitcher.currentTheme">
+            <a-tab-pane key="tpl-basic" tab='{{ i18n "pages.xray.basicTemplate"}}' :style="{ paddingTop: '20px' }">
+            {{ template "settings/xray/basics" . }}
+            </a-tab-pane>
+            <a-tab-pane key="tpl-routing" tab='{{ i18n "pages.xray.Routings"}}' :style="{ paddingTop: '20px' }">
+            {{ template "settings/xray/routing" . }}
+            </a-tab-pane>
+            <a-tab-pane key="tpl-outbound" tab='{{ i18n "pages.xray.Outbounds"}}' force-render="true">
+            {{ template "settings/xray/outbounds" . }}
+            </a-tab-pane>
+            <a-tab-pane key="tpl-reverse" tab='{{ i18n "pages.xray.outbound.reverse"}}' :style="{ paddingTop: '20px' }" force-render="true">
+            {{ template "settings/xray/reverse" . }}
+            </a-tab-pane>
+            <a-tab-pane key="tpl-balancer" tab='{{ i18n "pages.xray.Balancers"}}' :style="{ paddingTop: '20px' }" force-render="true">
+            {{ template "settings/xray/balancers" . }}
+            </a-tab-pane>
+            <a-tab-pane key="tpl-dns" tab='DNS' :style="{ paddingTop: '20px' }" force-render="true">
+            {{ template "settings/xray/dns" . }}
+            </a-tab-pane>
+            <a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' :style="{ paddingTop: '20px' }" force-render="true">
+            {{ template "settings/xray/advanced" . }}
+            </a-tab-pane>
+        </a-tabs>
+        </a-space>
+    </a-spin>
+    </a-layout-content>
+</a-layout>
+</a-layout>
+{{template "page/body_scripts" .}}
+<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
+<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
+<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
+<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
+<script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
+<script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
+<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
+<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
+<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
 {{template "component/aSidebar" .}}
 {{template "component/aThemeSwitch" .}}
 {{template "component/aTableSortable" .}}
@@ -1410,5 +1409,4 @@
         },
     });
 </script>
-</body>
-</html>
+{{ template "page/body_end" .}}