mirror of
				https://github.com/MHSanaei/3x-ui.git
				synced 2025-10-30 20:02:51 +00:00 
			
		
		
		
	Compare commits
	
		
			9 commits
		
	
	
		
			bacbcc61a4
			...
			f7a3ebf2f3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f7a3ebf2f3 | ||
|   | c8c0e77714 | ||
|   | 49b8f46864 | ||
|   | cad07be847 | ||
|   | 4b20f16024 | ||
|   | d642774a44 | ||
|   | 1644904755 | ||
|   | 5c10035bd9 | ||
|   | 2e6faf69e6 | 
					 15 changed files with 2785 additions and 2542 deletions
				
			
		|  | @ -2150,7 +2150,7 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass { | |||
| Inbound.ShadowsocksSettings = class extends Inbound.Settings { | ||||
|     constructor(protocol, | ||||
|         method = SSMethods.BLAKE3_AES_256_GCM, | ||||
|         password = RandomUtil.randomShadowsocksPassword(), | ||||
|         password = '', | ||||
|         network = 'tcp,udp', | ||||
|         shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()], | ||||
|         ivCheck = false, | ||||
|  | @ -2188,7 +2188,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings { | |||
| Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { | ||||
|     constructor( | ||||
|         method = '', | ||||
|         password = RandomUtil.randomShadowsocksPassword(), | ||||
|         password = '', | ||||
|         email = RandomUtil.randomLowerAndNum(8), | ||||
|         limitIp = 0, | ||||
|         totalGB = 0, | ||||
|  |  | |||
|  | @ -138,8 +138,14 @@ class RandomUtil { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static randomShadowsocksPassword() { | ||||
|         const array = new Uint8Array(32); | ||||
|     static randomShadowsocksPassword(method = SSMethods.BLAKE3_AES_256_GCM) { | ||||
|         let length = 32; | ||||
| 
 | ||||
|         if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) { | ||||
|             length = 16;  | ||||
|         } | ||||
| 
 | ||||
|         const array = new Uint8Array(length); | ||||
| 
 | ||||
|         window.crypto.getRandomValues(array); | ||||
| 
 | ||||
|  | @ -789,6 +795,25 @@ class LanguageManager { | |||
|             if (window.navigator) { | ||||
|                 lang = window.navigator.language || window.navigator.userLanguage; | ||||
| 
 | ||||
|                 const simularLangs = [ | ||||
|                     ["ar", this.supportedLanguages[0].value], | ||||
|                     ["fa", this.supportedLanguages[2].value], | ||||
|                     ["ja", this.supportedLanguages[5].value], | ||||
|                     ["ru", this.supportedLanguages[6].value], | ||||
|                     ["vi", this.supportedLanguages[7].value], | ||||
|                     ["es", this.supportedLanguages[8].value], | ||||
|                     ["id", this.supportedLanguages[9].value], | ||||
|                     ["uk", this.supportedLanguages[10].value], | ||||
|                     ["tr", this.supportedLanguages[11].value], | ||||
|                     ["pt", this.supportedLanguages[12].value], | ||||
|                 ] | ||||
| 
 | ||||
|                 simularLangs.forEach((pair) => { | ||||
|                     if (lang === pair[0]) { | ||||
|                         lang = pair[1]; | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 if (LanguageManager.isSupportLanguage(lang)) { | ||||
|                     CookieManager.setCookie("lang", lang, 150); | ||||
|                 } else { | ||||
|  |  | |||
|  | @ -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}} | ||||
|  | @ -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
									
								
								web/html/common/page.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								web/html/common/page.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 }} | ||||
|  | @ -41,6 +41,7 @@ | |||
|   </template> | ||||
| </template> | ||||
| <template slot="client" slot-scope="text, client"> | ||||
|   <a-space direction="horizontal" :size="2"> | ||||
|     <a-tooltip> | ||||
|       <template slot="title"> | ||||
|         <template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template> | ||||
|  | @ -48,7 +49,20 @@ | |||
|         <template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template> | ||||
|       </template> | ||||
|       <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge> | ||||
|   </a-tooltip> [[ client.email ]] | ||||
|     </a-tooltip> | ||||
|     <a-space direction="vertical" :size="2"> | ||||
|       <span class="client-email">[[ client.email ]]</span> | ||||
|       <template v-if="client.comment && client.comment.trim()"> | ||||
|         <a-tooltip v-if="client.comment.length > 50" :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|           <template slot="title"> | ||||
|             [[ client.comment ]] | ||||
|           </template> | ||||
|           <span class="client-comment">[[ client.comment.substring(0, 47) + '...' ]]</span> | ||||
|         </a-tooltip> | ||||
|         <span v-else class="client-comment">[[ client.comment ]]</span> | ||||
|       </template> | ||||
|     </a-space> | ||||
|   </a-space> | ||||
| </template> | ||||
| <template slot="traffic" slot-scope="text, client"> | ||||
|   <a-popover :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|                     <span>{{ i18n "reset" }}</span> | ||||
|                 </template> | ||||
|                 {{ i18n "password" }} | ||||
|                 <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"@click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon> | ||||
|                 <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon> | ||||
|                 <a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon> | ||||
|             </a-tooltip> | ||||
|         </template> | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ | |||
|             <a-tooltip> | ||||
|                 <template slot="title"> | ||||
|                     <span>{{ i18n "reset" }}</span> | ||||
|                 </template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon> | ||||
|                 </template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon> | ||||
|             </a-tooltip> | ||||
|         </template> | ||||
|         <a-input v-model.trim="inbound.settings.password"></a-input> | ||||
|  |  | |||
|  | @ -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; | ||||
|  | @ -79,6 +77,19 @@ | |||
|     max-width: 200px; | ||||
|     overflow: hidden; | ||||
|   } | ||||
|   .client-comment { | ||||
|     font-size: 12px; | ||||
|     opacity: 0.75; | ||||
|     cursor: help; | ||||
|   } | ||||
|   .client-email { | ||||
|     font-weight: 500; | ||||
|   } | ||||
|   .client-popup-item { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 5px; | ||||
|   } | ||||
|   .online-animation .ant-badge-status-dot { | ||||
|     animation: onlineAnimation 1.2s linear infinite; | ||||
|   } | ||||
|  | @ -130,15 +141,16 @@ | |||
|     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"> | ||||
|     <a-layout-content> | ||||
|       <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> | ||||
|       <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> | ||||
|         <transition name="list" appear> | ||||
|           <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }" | ||||
|           <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" | ||||
|             message='{{ i18n "secAlertTitle" }}' | ||||
|             color="red" | ||||
|             description='{{ i18n "secAlertSsl" }}' | ||||
|  | @ -146,6 +158,13 @@ | |||
|           </a-alert> | ||||
|         </transition> | ||||
|         <transition name="list" appear> | ||||
|           <a-row v-if="!loadingStates.fetched"> | ||||
|             <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 :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else> | ||||
|             <a-col> | ||||
|               <a-card size="small" :style="{ padding: '16px' }" hoverable> | ||||
|                 <a-row> | ||||
|                   <a-col :sm="12" :md="6"> | ||||
|  | @ -208,8 +227,8 @@ | |||
|                   </a-col> | ||||
|                 </a-row> | ||||
|               </a-card> | ||||
|         </transition> | ||||
|         <transition name="list" appear> | ||||
|             </a-col> | ||||
|             <a-col> | ||||
|               <a-card hoverable> | ||||
|                 <template #title> | ||||
|                   <a-space direction="horizontal"> | ||||
|  | @ -382,25 +401,57 @@ | |||
|                         <a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> | ||||
|                         <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                           <template slot="content"> | ||||
|                         <div v-for="clientEmail in clientCount[dbInbound.id].deactive"><span>[[ clientEmail ]]</span></div> | ||||
|                             <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" class="client-popup-item"> | ||||
|                               <span>[[ clientEmail ]]</span> | ||||
|                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                 <template #title> | ||||
|                                   [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                 </template> | ||||
|                                 <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                               </a-tooltip> | ||||
|                             </div> | ||||
|                           </template> | ||||
|                           <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> | ||||
|                         </a-popover> | ||||
|                         <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                           <template slot="content"> | ||||
|                         <div v-for="clientEmail in clientCount[dbInbound.id].depleted"><span>[[ clientEmail ]]</span></div> | ||||
|                             <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" class="client-popup-item"> | ||||
|                               <span>[[ clientEmail ]]</span> | ||||
|                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                 <template #title> | ||||
|                                   [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                 </template> | ||||
|                                 <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                               </a-tooltip> | ||||
|                             </div> | ||||
|                           </template> | ||||
|                           <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> | ||||
|                         </a-popover> | ||||
|                         <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                           <template slot="content"> | ||||
|                         <div v-for="clientEmail in clientCount[dbInbound.id].expiring"><span>[[ clientEmail ]]</span></div> | ||||
|                             <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" class="client-popup-item"> | ||||
|                               <span>[[ clientEmail ]]</span> | ||||
|                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                 <template #title> | ||||
|                                   [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                 </template> | ||||
|                                 <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                               </a-tooltip> | ||||
|                             </div> | ||||
|                           </template> | ||||
|                           <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag> | ||||
|                         </a-popover> | ||||
|                         <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                           <template slot="content"> | ||||
|                         <div v-for="clientEmail in clientCount[dbInbound.id].online"><span>[[ clientEmail ]]</span></div> | ||||
|                             <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" class="client-popup-item"> | ||||
|                               <span>[[ clientEmail ]]</span> | ||||
|                               <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                 <template #title> | ||||
|                                   [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                 </template> | ||||
|                                 <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                               </a-tooltip> | ||||
|                             </div> | ||||
|                           </template> | ||||
|                           <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag> | ||||
|                         </a-popover> | ||||
|  | @ -479,25 +530,57 @@ | |||
|                                 <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag> | ||||
|                                 <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                   <template slot="content"> | ||||
|                                 <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> | ||||
|                                     <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" class="client-popup-item"> | ||||
|                                       <span>[[ clientEmail ]]</span> | ||||
|                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                         <template #title> | ||||
|                                           [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                         </template> | ||||
|                                         <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                                       </a-tooltip> | ||||
|                                     </div> | ||||
|                                   </template> | ||||
|                                   <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> | ||||
|                                 </a-popover> | ||||
|                                 <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                   <template slot="content"> | ||||
|                                 <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> | ||||
|                                     <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" class="client-popup-item"> | ||||
|                                       <span>[[ clientEmail ]]</span> | ||||
|                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                         <template #title> | ||||
|                                           [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                         </template> | ||||
|                                         <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                                       </a-tooltip> | ||||
|                                     </div> | ||||
|                                   </template> | ||||
|                                   <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> | ||||
|                                 </a-popover> | ||||
|                                 <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                   <template slot="content"> | ||||
|                                 <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> | ||||
|                                     <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" class="client-popup-item"> | ||||
|                                       <span>[[ clientEmail ]]</span> | ||||
|                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                         <template #title> | ||||
|                                           [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                         </template> | ||||
|                                         <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                                       </a-tooltip> | ||||
|                                     </div> | ||||
|                                   </template> | ||||
|                                   <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag> | ||||
|                                 </a-popover> | ||||
|                                 <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                   <template slot="content"> | ||||
|                                 <p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p> | ||||
|                                     <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" class="client-popup-item"> | ||||
|                                       <span>[[ clientEmail ]]</span> | ||||
|                                       <a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> | ||||
|                                         <template #title> | ||||
|                                           [[ getClientWithComment(clientEmail, dbInbound.id).comment ]] | ||||
|                                         </template> | ||||
|                                         <a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> | ||||
|                                       </a-tooltip> | ||||
|                                     </div> | ||||
|                                   </template> | ||||
|                                   <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag> | ||||
|                                 </a-popover> | ||||
|  | @ -575,12 +658,14 @@ | |||
|                   </a-table> | ||||
|                 </a-space> | ||||
|               </a-card>  | ||||
|             </a-col> | ||||
|           </a-row> | ||||
|         </transition> | ||||
|       </a-spin> | ||||
|     </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> | ||||
|  | @ -589,6 +674,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", | ||||
|  | @ -683,7 +775,10 @@ | |||
|         data: { | ||||
|             themeSwitcher, | ||||
|             persianDatepicker, | ||||
|             spinning: false, | ||||
|             loadingStates: { | ||||
|               fetched: false, | ||||
|               spinning: false | ||||
|             }, | ||||
|             inbounds: [], | ||||
|             dbInbounds: [], | ||||
|             searchKey: '', | ||||
|  | @ -714,7 +809,18 @@ | |||
|         }, | ||||
|         methods: { | ||||
|             loading(spinning = true) { | ||||
|                 this.spinning = spinning; | ||||
|                 this.loadingStates.spinning = spinning; | ||||
|             }, | ||||
|             getClientWithComment(email, inboundId) { | ||||
|                 const dbInbound = this.dbInbounds.find(inbound => inbound.id === inboundId); | ||||
|                 if (!dbInbound) return { email, comment: '' }; | ||||
|                  | ||||
|                 const inboundSettings = JSON.parse(dbInbound.settings); | ||||
|                 if (inboundSettings.clients) { | ||||
|                     const client = inboundSettings.clients.find(c => c.email === email); | ||||
|                     return client ? { email: client.email, comment: client.comment || '' } : { email, comment: '' }; | ||||
|                 } | ||||
|                 return { email, comment: '' }; | ||||
|             }, | ||||
|             async getDBInbounds() { | ||||
|                 this.refreshing = true; | ||||
|  | @ -725,6 +831,7 @@ | |||
|                 } | ||||
| 
 | ||||
|                 await this.getOnlineUsers(); | ||||
|                  | ||||
|                 this.setInbounds(msg.obj); | ||||
|                 setTimeout(() => { | ||||
|                     this.refreshing = false; | ||||
|  | @ -776,6 +883,9 @@ | |||
|                         this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound); | ||||
|                     } | ||||
|                 } | ||||
|                 if (!this.loadingStates.fetched) { | ||||
|                     this.loadingStates.fetched = true | ||||
|                 } | ||||
|                 if(this.enableFilter){ | ||||
|                     this.filterInbounds(); | ||||
|                 } else { | ||||
|  | @ -1447,9 +1557,9 @@ | |||
|             }, | ||||
|             async manualRefresh() { | ||||
|                 if (!this.refreshing) { | ||||
|                     this.spinning = true; | ||||
|                     this.loadingStates.spinning = true; | ||||
|                     await this.getDBInbounds(); | ||||
|                     this.spinning = false; | ||||
|                     this.loadingStates.spinning = false; | ||||
|                 } | ||||
|             }, | ||||
|             pagination(obj){ | ||||
|  | @ -1519,13 +1629,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" .}} | ||||
|  | @ -1,20 +1,9 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| {{template "head" .}} | ||||
| {{ template "page/head_start" .}} | ||||
| <style> | ||||
|   @media (min-width: 769px) { | ||||
|     .ant-layout-content { | ||||
|       margin: 24px 16px; | ||||
|     } | ||||
|     .ant-card-hoverable { | ||||
|       margin-inline: 0.3rem; | ||||
|     } | ||||
|     .ant-alert-error { | ||||
|       margin-inline: 0.3rem; | ||||
|     } | ||||
|   } | ||||
|   .ant-col-sm-24 { | ||||
|     margin-top: 10px; | ||||
|   } | ||||
|   .ant-card-dark h2 { | ||||
|     color: var(--dark-color-text-primary); | ||||
|  | @ -79,15 +68,16 @@ | |||
|     } | ||||
|   } | ||||
| </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"> | ||||
|     <a-layout-content> | ||||
|         <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"> | ||||
|       <a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip"> | ||||
|         <transition name="list" appear> | ||||
|             <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }" | ||||
|           <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" | ||||
|             message='{{ i18n "secAlertTitle" }}' | ||||
|             color="red" | ||||
|             description='{{ i18n "secAlertSsl" }}' | ||||
|  | @ -96,15 +86,15 @@ | |||
|         </transition> | ||||
|         <transition name="list" appear> | ||||
|           <template> | ||||
|               <a-row v-if="!status.isLoaded"> | ||||
|             <a-row v-if="!loadingStates.fetched"> | ||||
|               <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-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else> | ||||
|               <a-col> | ||||
|                 <a-card hoverable> | ||||
|                     <a-row> | ||||
|                   <a-row :gutter="[0, isMobile ? 16 : 0]"> | ||||
|                     <a-col :sm="24" :md="12"> | ||||
|                       <a-row> | ||||
|                         <a-col :span="12" :style="{ textAlign: 'center' }"> | ||||
|  | @ -154,7 +144,7 @@ | |||
|                     </a-col> | ||||
|                   </a-row> | ||||
|                 </a-card> | ||||
|                 </a-row> | ||||
|               </a-col> | ||||
|               <a-col :sm="24" :lg="12"> | ||||
|                 <a-card hoverable> | ||||
|                   <template #title> | ||||
|  | @ -237,7 +227,7 @@ | |||
|                     </a-tag> | ||||
|                   </a> | ||||
|                   <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank"> | ||||
|                       <a-tag> | ||||
|                     <a-tag color="purple"> | ||||
|                       <span>{{ i18n "pages.index.documentation" }}</span> | ||||
|                     </a-tag> | ||||
|                   </a> | ||||
|  | @ -456,7 +446,7 @@ | |||
|     </a-list> | ||||
|   </a-modal> | ||||
| </a-layout> | ||||
| {{template "js" .}} | ||||
| {{template "page/body_scripts" .}} | ||||
| {{template "component/aSidebar" .}} | ||||
| {{template "component/aThemeSwitch" .}} | ||||
| {{template "component/aCustomStatistic" .}} | ||||
|  | @ -489,7 +479,7 @@ | |||
|     } | ||||
| 
 | ||||
|     class Status { | ||||
|         constructor(data, isLoaded = false) { | ||||
|         constructor(data) { | ||||
|             this.cpu = new CurTotal(0, 0); | ||||
|             this.cpuCores = 0; | ||||
|             this.logicalPro = 0; | ||||
|  | @ -513,7 +503,6 @@ | |||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             this.isLoaded = isLoaded; | ||||
|             this.cpu = new CurTotal(data.cpu, 100); | ||||
|             this.cpuCores = data.cpuCores; | ||||
|             this.logicalPro = data.logicalPro; | ||||
|  | @ -633,32 +622,39 @@ | |||
|         mixins: [MediaQueryMixin], | ||||
|         data: { | ||||
|             themeSwitcher, | ||||
|             loadingStates: { | ||||
|               fetched: false, | ||||
|               spinning: false | ||||
|             }, | ||||
|             status: new Status(), | ||||
|             versionModal, | ||||
|             logModal, | ||||
|             backupModal, | ||||
|             spinning: false, | ||||
|             loadingTip: '{{ i18n "loading"}}', | ||||
|             showAlert: false, | ||||
|             showIp: false | ||||
|         }, | ||||
|         methods: { | ||||
|             loading(spinning, tip = '{{ i18n "loading"}}') { | ||||
|                 this.spinning = spinning; | ||||
|                 this.loadingStates.spinning = spinning; | ||||
|                 this.loadingTip = tip; | ||||
|             }, | ||||
|             async getStatus() { | ||||
|                 try { | ||||
|                     const msg = await HttpUtil.post('/server/status'); | ||||
|                     if (msg.success) { | ||||
|                         if (!this.loadingStates.fetched) { | ||||
|                             this.loadingStates.fetched = true; | ||||
|                         } | ||||
| 
 | ||||
|                         this.setStatus(msg.obj, true); | ||||
|                     } | ||||
|                 } catch (e) { | ||||
|                     console.error("Failed to get status:", e); | ||||
|                 } | ||||
|             }, | ||||
|             setStatus(data, isLoaded = false) { | ||||
|               this.status = new Status(data, isLoaded); | ||||
|             setStatus(data) { | ||||
|                 this.status = new Status(data); | ||||
|             }, | ||||
|             async openSelectV2rayVersion() { | ||||
|                 this.loading(true); | ||||
|  | @ -788,5 +784,4 @@ | |||
|         }, | ||||
|     }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| {{ template "page/body_end" .}} | ||||
|  | @ -1,6 +1,4 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| {{template "head" .}} | ||||
| {{ template "page/head_start" .}} | ||||
| <style> | ||||
|   html * { | ||||
|     -webkit-font-smoothing: antialiased; | ||||
|  | @ -453,8 +451,9 @@ | |||
|     margin: 2px 0 4px; | ||||
|   } | ||||
| </style> | ||||
| {{ template "page/head_end" .}} | ||||
| 
 | ||||
| <body> | ||||
| {{ 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' }"> | ||||
|  | @ -543,7 +542,7 @@ | |||
|     </a-layout-content> | ||||
|   </transition> | ||||
| </a-layout> | ||||
|   {{template "js" .}} | ||||
| {{template "page/body_scripts" .}} | ||||
| {{template "component/aThemeSwitch" .}} | ||||
| <script> | ||||
|   const app = new Vue({ | ||||
|  | @ -622,5 +621,4 @@ | |||
|     } | ||||
|   }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| {{ template "page/body_end" .}} | ||||
|  | @ -1,6 +1,4 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| {{template "head" .}} | ||||
| {{ template "page/head_start" .}} | ||||
| <style> | ||||
|   @media (min-width: 769px) { | ||||
|     .ant-layout-content { | ||||
|  | @ -60,14 +58,16 @@ | |||
|     margin-block-end: 12px; | ||||
|   } | ||||
| </style> | ||||
| <body> | ||||
| {{ 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"}}'> | ||||
|       <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> | ||||
|         <transition name="list" appear> | ||||
|             <a-alert type="error" v-if="confAlerts.length>0" :style="{ marginBottom: '10px' }" | ||||
|           <a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }" | ||||
|               message='{{ i18n "secAlertTitle" }}' | ||||
|               color="red" | ||||
|               show-icon closable> | ||||
|  | @ -77,8 +77,16 @@ | |||
|             </template> | ||||
|           </a-alert> | ||||
|         </transition> | ||||
|           <a-space direction="vertical"> | ||||
|             <a-card hoverable :style="{ marginBottom: '.5rem', overflowX: 'hidden' }"> | ||||
|         <transition name="list" appear> | ||||
|           <template> | ||||
|             <a-row v-if="!loadingStates.fetched"> | ||||
|               <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 :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else> | ||||
|               <a-col> | ||||
|                 <a-card hoverable> | ||||
|                   <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }"> | ||||
|                     <a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> | ||||
|                       <a-space direction="horizontal"> | ||||
|  | @ -99,29 +107,54 @@ | |||
|                     </a-col> | ||||
|                   </a-row> | ||||
|                 </a-card> | ||||
|               </a-col> | ||||
|               <a-col> | ||||
|                 <a-tabs default-active-key="1"> | ||||
|               <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings" }}' :style="{ paddingTop: '20px' }"> | ||||
|                   <a-tab-pane key="1" :style="{ paddingTop: '20px' }"> | ||||
|                     <template #tab> | ||||
|                       <a-icon type="setting"></a-icon> | ||||
|                       <span>{{ i18n "pages.settings.panelSettings" }}</span> | ||||
|                     </template> | ||||
|                     {{ template "settings/panel/general" . }} | ||||
|                   </a-tab-pane> | ||||
|               <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings" }}' :style="{ paddingTop: '20px' }"> | ||||
|                   <a-tab-pane key="2" :style="{ paddingTop: '20px' }"> | ||||
|                     <template #tab> | ||||
|                       <a-icon type="safety"></a-icon> | ||||
|                       <span>{{ i18n "pages.settings.securitySettings" }}</span> | ||||
|                     </template> | ||||
|                     {{ template "settings/panel/security" . }} | ||||
|                   </a-tab-pane> | ||||
|               <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings" }}' :style="{ paddingTop: '20px' }"> | ||||
|                   <a-tab-pane key="3" :style="{ paddingTop: '20px' }"> | ||||
|                     <template #tab> | ||||
|                       <a-icon type="message"></a-icon> | ||||
|                       <span>{{ i18n "pages.settings.TGBotSettings" }}</span> | ||||
|                     </template> | ||||
|                     {{ template "settings/panel/telegram" . }} | ||||
|                   </a-tab-pane> | ||||
|               <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}' :style="{ paddingTop: '20px' }"> | ||||
|                   <a-tab-pane key="4" :style="{ paddingTop: '20px' }"> | ||||
|                     <template #tab> | ||||
|                       <a-icon type="cloud-server"></a-icon> | ||||
|                       <span>{{ i18n "pages.settings.subSettings" }}</span> | ||||
|                     </template> | ||||
|                     {{ 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' }"> | ||||
|                   <a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }"> | ||||
|                     <template #tab> | ||||
|                       <a-icon type="code"></a-icon> | ||||
|                       <span>{{ i18n "pages.settings.subSettings" }} (JSON)</span> | ||||
|                     </template> | ||||
|                     {{ template "settings/panel/subscription/json" . }} | ||||
|                   </a-tab-pane> | ||||
|                 </a-tabs> | ||||
|           </a-space> | ||||
|               </a-col> | ||||
|             </a-row> | ||||
|           </template> | ||||
|         </transition> | ||||
|       </a-spin> | ||||
|     </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/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script> | ||||
| <script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script> | ||||
|  | @ -132,10 +165,14 @@ | |||
| <script> | ||||
|   const app = new Vue({ | ||||
|     delimiters: ['[[', ']]'], | ||||
|     mixins: [MediaQueryMixin], | ||||
|     el: '#app', | ||||
|     data: { | ||||
|       themeSwitcher, | ||||
|       spinning: false, | ||||
|       loadingStates: { | ||||
|         fetched: false, | ||||
|         spinning: false | ||||
|       }, | ||||
|       oldAllSetting: new AllSetting(), | ||||
|       allSetting: new AllSetting(), | ||||
|       saveBtnDisable: true, | ||||
|  | @ -248,13 +285,16 @@ | |||
|     }, | ||||
|     methods: { | ||||
|       loading(spinning = true) { | ||||
|         this.spinning = spinning; | ||||
|         this.loadingStates.spinning = spinning; | ||||
|       }, | ||||
|       async getAllSetting() { | ||||
|         this.loading(true); | ||||
|         const msg = await HttpUtil.post("/panel/setting/all"); | ||||
|         this.loading(false); | ||||
| 
 | ||||
|         if (msg.success) { | ||||
|           if (!this.loadingStates.fetched) { | ||||
|             this.loadingStates.fetched = true | ||||
|           } | ||||
| 
 | ||||
|           this.oldAllSetting = new AllSetting(msg.obj); | ||||
|           this.allSetting = new AllSetting(msg.obj); | ||||
|           app.changeRemarkSample(); | ||||
|  | @ -508,7 +548,7 @@ | |||
|           if (!this.allSetting) return []; | ||||
|           var alerts = [] | ||||
|           if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}'); | ||||
|           if (this.allSetting.webPort == 54321) alerts.push('{{ i18n "secAlertPanelPort" }}'); | ||||
|           if (this.allSetting.webPort === 2053) alerts.push('{{ i18n "secAlertPanelPort" }}'); | ||||
|           panelPath = window.location.pathname.split('/').length < 4 | ||||
|           if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}'); | ||||
|           if (this.allSetting.subEnable) { | ||||
|  | @ -531,5 +571,4 @@ | |||
|     } | ||||
|   }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| {{ template "page/body_end" .}} | ||||
|  | @ -1,5 +1,5 @@ | |||
| {{define "settings/xray/advanced"}} | ||||
| <a-space direction="vertical" size="small"> | ||||
| <a-space direction="vertical" size="small" :style="{ marginTop: '20px' }"> | ||||
|     <a-list-item-meta title='{{ i18n "pages.xray.Template"}}' | ||||
|         description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta> | ||||
|     <a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" :style="{ margin: '10px 0' }" | ||||
|  |  | |||
|  | @ -1,74 +1,75 @@ | |||
| <!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 { | ||||
|       margin: 24px 16px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @media (max-width: 768px) { | ||||
|     .ant-tabs-nav .ant-tabs-tab { | ||||
|       margin: 0; | ||||
|       padding: 12px .5rem; | ||||
|     } | ||||
| 
 | ||||
|     .ant-table-thead>tr>th, | ||||
|     .ant-table-tbody>tr>td { | ||||
|       padding: 10px 0px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .ant-tabs-bar { | ||||
|     margin: 0; | ||||
|   } | ||||
| 
 | ||||
|   .ant-list-item { | ||||
|     display: block; | ||||
|   } | ||||
| 
 | ||||
|   .ant-list-item>li { | ||||
|     padding: 10px 20px !important; | ||||
|   } | ||||
| 
 | ||||
|   .ant-collapse-content-box .ant-alert { | ||||
|     margin-block-end: 12px; | ||||
|   } | ||||
| </style> | ||||
| <body> | ||||
| {{ 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"}}'> | ||||
|       <a-spin :spinning="loadingStates.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 type="error" v-if="showAlert && loadingStates.fetched" :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' }"> | ||||
|         <transition name="list" appear> | ||||
|           <a-row v-if="!loadingStates.fetched"> | ||||
|             <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 :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else> | ||||
|             <a-col> | ||||
|               <a-card hoverable> | ||||
|                 <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"> | ||||
|                       <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> | ||||
|  | @ -80,45 +81,89 @@ | |||
|                   <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-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); }" | ||||
|             </a-col> | ||||
|             <a-col> | ||||
|               <a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }" | ||||
|                 :class="themeSwitcher.currentTheme"> | ||||
|               <a-tab-pane key="tpl-basic" tab='{{ i18n "pages.xray.basicTemplate"}}' :style="{ paddingTop: '20px' }"> | ||||
|                 <a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="setting"></a-icon> | ||||
|                     <span>{{ i18n "pages.xray.basicTemplate"}}</span> | ||||
|                   </template> | ||||
|                   {{ template "settings/xray/basics" . }} | ||||
|                 </a-tab-pane> | ||||
|               <a-tab-pane key="tpl-routing" tab='{{ i18n "pages.xray.Routings"}}' :style="{ paddingTop: '20px' }"> | ||||
|                 <a-tab-pane key="tpl-routing" :style="{ paddingTop: '20px' }"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="swap"></a-icon> | ||||
|                     <span>{{ i18n "pages.xray.Routings"}}</span> | ||||
|                   </template> | ||||
|                   {{ template "settings/xray/routing" . }} | ||||
|                 </a-tab-pane> | ||||
|               <a-tab-pane key="tpl-outbound" tab='{{ i18n "pages.xray.Outbounds"}}' force-render="true"> | ||||
|                 <a-tab-pane key="tpl-outbound" force-render="true"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="upload"></a-icon> | ||||
|                     <span>{{ i18n "pages.xray.Outbounds"}}</span> | ||||
|                   </template> | ||||
|                   {{ 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"> | ||||
|                 <a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }" force-render="true"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="import"></a-icon> | ||||
|                     <span>{{ i18n "pages.xray.outbound.reverse"}}</span> | ||||
|                   </template> | ||||
|                   {{ template "settings/xray/reverse" . }} | ||||
|                 </a-tab-pane> | ||||
|               <a-tab-pane key="tpl-balancer" tab='{{ i18n "pages.xray.Balancers"}}' :style="{ paddingTop: '20px' }" force-render="true"> | ||||
|                 <a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" force-render="true"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="cluster"></a-icon> | ||||
|                     <span>{{ i18n "pages.xray.Balancers"}}</span> | ||||
|                   </template> | ||||
|                   {{ template "settings/xray/balancers" . }} | ||||
|                 </a-tab-pane> | ||||
|               <a-tab-pane key="tpl-dns" tab='DNS' :style="{ paddingTop: '20px' }" force-render="true"> | ||||
|                 <a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" force-render="true"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="database"></a-icon> | ||||
|                     <span>DNS</span> | ||||
|                   </template> | ||||
|                   {{ template "settings/xray/dns" . }} | ||||
|                 </a-tab-pane> | ||||
|               <a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' :style="{ paddingTop: '20px' }" force-render="true"> | ||||
|                 <a-tab-pane key="tpl-advanced" force-render="true"> | ||||
|                   <template #tab> | ||||
|                     <a-icon type="code"></a-icon> | ||||
|                     <span>{{ i18n "pages.xray.advancedTemplate"}}</span> | ||||
|                   </template> | ||||
|                   {{ template "settings/xray/advanced" . }} | ||||
|                 </a-tab-pane> | ||||
|               </a-tabs> | ||||
|           </a-space> | ||||
|             </a-col> | ||||
|           </a-row> | ||||
|         </transition> | ||||
|       </a-spin> | ||||
|     </a-layout-content> | ||||
|   </a-layout> | ||||
| </a-layout> | ||||
| {{template "js" .}} | ||||
| {{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" .}} | ||||
|  | @ -134,20 +179,28 @@ | |||
| <script> | ||||
|   const rulesColumns = [ | ||||
|     { title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } }, | ||||
|         { title: '{{ i18n "pages.xray.rules.source"}}', children: [ | ||||
|     { | ||||
|       title: '{{ i18n "pages.xray.rules.source"}}', children: [ | ||||
|         { title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true }, | ||||
|             { title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true } ]}, | ||||
|         { title: '{{ i18n "pages.inbounds.network"}}', children: [ | ||||
|         { title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }] | ||||
|     }, | ||||
|     { | ||||
|       title: '{{ i18n "pages.inbounds.network"}}', children: [ | ||||
|         { title: 'L4', dataIndex: 'network', align: 'center', width: 10 }, | ||||
|         { title: '{{ i18n "protocol" }}', dataIndex: 'protocol', align: 'center', width: 15, ellipsis: true }, | ||||
|             { title: 'Attrs', dataIndex: 'attrs', align: 'center', width: 10, ellipsis: true } ]},  | ||||
|         { title: '{{ i18n "pages.xray.rules.dest"}}', children: [ | ||||
|         { title: 'Attrs', dataIndex: 'attrs', align: 'center', width: 10, ellipsis: true }] | ||||
|     }, | ||||
|     { | ||||
|       title: '{{ i18n "pages.xray.rules.dest"}}', children: [ | ||||
|         { title: 'IP', dataIndex: 'ip', align: 'center', width: 20, ellipsis: true }, | ||||
|         { title: '{{ i18n "pages.xray.outbound.domain" }}', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true }, | ||||
|             { title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]}, | ||||
|         { title: '{{ i18n "pages.xray.rules.inbound"}}', children: [ | ||||
|         { title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }] | ||||
|     }, | ||||
|     { | ||||
|       title: '{{ i18n "pages.xray.rules.inbound"}}', children: [ | ||||
|         { title: '{{ i18n "pages.xray.outbound.tag" }}', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true }, | ||||
|             { title: '{{ i18n "pages.inbounds.client" }}', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]}, | ||||
|         { title: '{{ i18n "pages.inbounds.client" }}', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }] | ||||
|     }, | ||||
|     { title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 17 }, | ||||
|     { title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 }, | ||||
|   ]; | ||||
|  | @ -201,7 +254,10 @@ | |||
|     data: { | ||||
|       themeSwitcher, | ||||
|       isDarkTheme: themeSwitcher.isDarkTheme, | ||||
|             spinning: false, | ||||
|       loadingStates: { | ||||
|         fetched: false, | ||||
|         spinning: false | ||||
|       }, | ||||
|       oldXraySetting: '', | ||||
|       xraySetting: '', | ||||
|       inboundTags: [], | ||||
|  | @ -330,7 +386,7 @@ | |||
|     }, | ||||
|     methods: { | ||||
|       loading(spinning = true) { | ||||
|                 this.spinning = spinning; | ||||
|         this.loadingStates.spinning = spinning; | ||||
|       }, | ||||
|       async getOutboundsTraffic() { | ||||
|         const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic"); | ||||
|  | @ -339,10 +395,13 @@ | |||
|         } | ||||
|       }, | ||||
|       async getXraySetting() { | ||||
|                 this.loading(true); | ||||
|         const msg = await HttpUtil.post("/panel/xray/"); | ||||
|                 this.loading(false); | ||||
| 
 | ||||
|         if (msg.success) { | ||||
|           if (!this.loadingStates.fetched) { | ||||
|             this.loadingStates.fetched = true | ||||
|           } | ||||
| 
 | ||||
|           result = JSON.parse(msg.obj); | ||||
|           xs = JSON.stringify(result.xraySetting, null, 2); | ||||
|           this.oldXraySetting = xs; | ||||
|  | @ -1410,5 +1469,4 @@ | |||
|     }, | ||||
|   }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
| {{ template "page/body_end" .}} | ||||
|  | @ -33,7 +33,7 @@ | |||
| "sure" = "Да" | ||||
| "encryption" = "Шифрование" | ||||
| "useIPv4ForHost" = "Использовать IPv4 для хоста" | ||||
| "transmission" = "Протокол" | ||||
| "transmission" = "Транспорт" | ||||
| "host" = "Хост" | ||||
| "path" = "Путь" | ||||
| "camouflage" = "Маскировка" | ||||
|  | @ -59,10 +59,10 @@ | |||
| "security" = "Безопасность" | ||||
| "secAlertTitle" = "Предупреждение системы безопасности" | ||||
| "secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения" | ||||
| "secAlertConf" = "Некоторые настройки уязвимы для атак. Рекомендуем усилить протоколы безопасности, чтобы предотвратить проблемы в будущем." | ||||
| "secAlertSSL" = "Ваше подключение к панели небезопасно. Пожалуйста, установите SSL сертификат для защиты данных." | ||||
| "secAlertPanelPort" = "Порт, на котором работает панель небезопасен. Пожалуйста, установите случайный или просто другой порт." | ||||
| "secAlertPanelURI" = "URI-адрес панели по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." | ||||
| "secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту." | ||||
| "secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных." | ||||
| "secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт." | ||||
| "secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным." | ||||
| "secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." | ||||
| "secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес." | ||||
| "emptyDnsDesc" = "Нет добавленных DNS-серверов." | ||||
|  | @ -75,8 +75,8 @@ | |||
| "theme" = "Тема" | ||||
| "dark" = "Темная" | ||||
| "ultraDark" = "Очень темная" | ||||
| "dashboard" = "Статус системы" | ||||
| "inbounds" = "Входящие подключения" | ||||
| "dashboard" = "Дашборд" | ||||
| "inbounds" = "Инбаунды" | ||||
| "settings" = "Настройки" | ||||
| "xray" = "Настройки Xray" | ||||
| "logout" = "Выход" | ||||
|  | @ -91,15 +91,15 @@ | |||
| "invalidFormData" = "Недопустимый формат данных" | ||||
| "emptyUsername" = "Введите имя пользователя" | ||||
| "emptyPassword" = "Введите пароль" | ||||
| "wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или код двухфакторной аутентификации." | ||||
| "successLogin" = "Успешный вход" | ||||
| "wrongUsernameOrPassword" = "Неверные данные учетной записи." | ||||
| "successLogin" = "Вы успешно вошли в аккаунт" | ||||
| 
 | ||||
| [pages.index] | ||||
| "title" = "Статус системы" | ||||
| "title" = "Дашборд" | ||||
| "cpu" = "ЦП" | ||||
| "logicalProcessors" = "Логические процессоры" | ||||
| "frequency" = "Частота" | ||||
| "swap" = "Swap" | ||||
| "swap" = "Файл подкачки" | ||||
| "storage" = "Диск" | ||||
| "memory" = "ОЗУ" | ||||
| "threads" = "Потоки" | ||||
|  | @ -136,7 +136,7 @@ | |||
| "geofileUpdateDialogDesc" = "Это обновит файл #filename#." | ||||
| "geofileUpdatePopover" = "Геофайл успешно обновлён" | ||||
| "dontRefresh" = "Установка в процессе. Не обновляйте страницу" | ||||
| "logs" = "Логи" | ||||
| "logs" = "Журнал" | ||||
| "config" = "Конфигурация" | ||||
| "backup" = "Резервная копия" | ||||
| "backupTitle" = "Резервная копия базы данных" | ||||
|  | @ -151,10 +151,10 @@ | |||
| "getConfigError" = "Произошла ошибка при получении конфигурационного файла" | ||||
| 
 | ||||
| [pages.inbounds] | ||||
| "title" = "Входящие подключения" | ||||
| "title" = "Инбаунды" | ||||
| "totalDownUp" = "Объем отправленного/полученного трафика" | ||||
| "totalUsage" = "Всего трафика" | ||||
| "inboundCount" = "Всего подключений" | ||||
| "inboundCount" = "Всего инбаундов" | ||||
| "operate" = "Меню" | ||||
| "enable" = "Включить" | ||||
| "remark" = "Примечание" | ||||
|  | @ -165,18 +165,18 @@ | |||
| "transportConfig" = "Транспорт" | ||||
| "expireDate" = "Дата окончания" | ||||
| "resetTraffic" = "Сброс трафика" | ||||
| "addInbound" = "Создать новое подключение" | ||||
| "addInbound" = "Создать инбаунд" | ||||
| "generalActions" = "Общие действия" | ||||
| "autoRefresh" = "Автообновление" | ||||
| "autoRefreshInterval" = "Интервал" | ||||
| "modifyInbound" = "Изменить входящее подключение" | ||||
| "deleteInbound" = "Удалить входящее подключение" | ||||
| "deleteInboundContent" = "Вы уверены, что хотите удалить входящее подключение?" | ||||
| "modifyInbound" = "Изменить инбаунд" | ||||
| "deleteInbound" = "Удалить инбаунд" | ||||
| "deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?" | ||||
| "deleteClient" = "Удалить клиента" | ||||
| "deleteClientContent" = "Вы уверены, что хотите удалить клиента?" | ||||
| "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" | ||||
| "inboundUpdateSuccess" = "Входящее подключение успешно обновлено." | ||||
| "inboundCreateSuccess" = "Входящее подключение успешно создано." | ||||
| "inboundUpdateSuccess" = "Инбаунд успешно обновлен." | ||||
| "inboundCreateSuccess" = "Инбаунд успешно создан." | ||||
| "copyLink" = "Копировать ссылку" | ||||
| "address" = "Адрес" | ||||
| "network" = "Сеть" | ||||
|  | @ -196,11 +196,11 @@ | |||
| "export" = "Экспорт ссылок" | ||||
| "clone" = "Клонировать" | ||||
| "cloneInbound" = "Клонировать" | ||||
| "cloneInboundContent" = "Будут клонированы все настройки входящих подключений, кроме списка клиентов, порта и IP-адреса прослушивания" | ||||
| "cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания" | ||||
| "cloneInboundOk" = "Клонировано" | ||||
| "resetAllTraffic" = "Сброс трафика всех подключений" | ||||
| "resetAllTrafficTitle" = "Сброс трафика всех подключений" | ||||
| "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?" | ||||
| "resetAllTraffic" = "Сброс трафика всех инбаундов" | ||||
| "resetAllTrafficTitle" = "Сброс трафика всех инбаундов" | ||||
| "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?" | ||||
| "resetInboundClientTraffics" = "Сброс трафика клиента" | ||||
| "resetInboundClientTrafficTitle" = "Сброс трафика клиентов" | ||||
| "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" | ||||
|  | @ -222,10 +222,10 @@ | |||
| "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" | ||||
| "info" = "Информация" | ||||
| "same" = "Тот же" | ||||
| "inboundData" = "Данные подключений" | ||||
| "exportInbound" = "Экспорт входящих подключений" | ||||
| "inboundData" = "Данные инбаундов" | ||||
| "exportInbound" = "Экспорт инбаундов" | ||||
| "import" = "Импортировать" | ||||
| "importInbound" = "Импорт входящих подключений" | ||||
| "importInbound" = "Импорт инбаундов" | ||||
| 
 | ||||
| [pages.client] | ||||
| "add" = "Создать клиента" | ||||
|  | @ -249,13 +249,13 @@ | |||
| "obtain" = "Получить" | ||||
| "updateSuccess" = "Обновление прошло успешно" | ||||
| "logCleanSuccess" = "Лог был очищен" | ||||
| "inboundsUpdateSuccess" = "Входящие подключения успешно обновлены" | ||||
| "inboundUpdateSuccess" = "Входящее подключение успешно обновлено" | ||||
| "inboundCreateSuccess" = "Входящее подключение успешно создано" | ||||
| "inboundDeleteSuccess" = "Входящее подключение успешно удалено" | ||||
| "inboundClientAddSuccess" = "Клиент(ы) входящего подключения добавлен(ы)" | ||||
| "inboundClientDeleteSuccess" = "Клиент входящего подключения удалён" | ||||
| "inboundClientUpdateSuccess" = "Клиент входящего подключения обновлён" | ||||
| "inboundsUpdateSuccess" = "Инбаунды успешно обновлены" | ||||
| "inboundUpdateSuccess" = "Инбаунд успешно обновлено" | ||||
| "inboundCreateSuccess" = "Инбаунд успешно создано" | ||||
| "inboundDeleteSuccess" = "Инбаунд успешно удалено" | ||||
| "inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)" | ||||
| "inboundClientDeleteSuccess" = "Клиент инбаунда удалён" | ||||
| "inboundClientUpdateSuccess" = "Клиент инбаунда обновлён" | ||||
| "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" | ||||
| "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" | ||||
| "resetAllTrafficSuccess" = "Весь трафик сброшен" | ||||
|  | @ -281,15 +281,15 @@ | |||
| [pages.settings] | ||||
| "title" = "Настройки" | ||||
| "save" = "Сохранить" | ||||
| "infoDesc" = "Каждое выполненное изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу" | ||||
| "infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу." | ||||
| "restartPanel" = "Перезапуск панели" | ||||
| "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" | ||||
| "restartPanelSuccess" = "Панель успешно перезапущена" | ||||
| "actions" = "Действия" | ||||
| "resetDefaultConfig" = "Восстановить настройки по умолчанию" | ||||
| "panelSettings" = "Настройки панели" | ||||
| "securitySettings" = "Настройки безопасности" | ||||
| "TGBotSettings" = "Настройки Telegram бота" | ||||
| "panelSettings" = "Панель" | ||||
| "securitySettings" = "Учетная запись" | ||||
| "TGBotSettings" = "Telegram" | ||||
| "panelListeningIP" = "IP-адрес для управления панелью" | ||||
| "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" | ||||
| "panelListeningDomain" = "Домен панели" | ||||
|  | @ -303,7 +303,7 @@ | |||
| "panelUrlPath" = "Корневой путь URL адреса панели" | ||||
| "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" | ||||
| "pageSize" = "Размер нумерации страниц" | ||||
| "pageSizeDesc" = "Определить размер страницы для таблицы входящих подключений. Установите 0, чтобы отключить" | ||||
| "pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить" | ||||
| "remarkModel" = "Модель примечания и символ разделения" | ||||
| "datepicker" = "Выбор даты" | ||||
| "datepickerPlaceholder" = "Выберите дату" | ||||
|  | @ -391,7 +391,7 @@ | |||
| [pages.xray] | ||||
| "title" = "Настройки Xray" | ||||
| "save" = "Сохранить" | ||||
| "restart" = "Перезапустить Xray" | ||||
| "restart" = "Перезапуск Xray" | ||||
| "restartSuccess" = "Xray успешно перезапущен" | ||||
| "stopSuccess" = "Xray успешно остановлен" | ||||
| "restartError" = "Произошла ошибка при перезапуске Xray." | ||||
|  | @ -402,7 +402,7 @@ | |||
| "generalConfigsDesc" = "Эти параметры описывают общие настройки" | ||||
| "logConfigs" = "Логи" | ||||
| "logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!" | ||||
| "blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам и веб-сайтами" | ||||
| "blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам" | ||||
| "basicRouting" = "Базовые соединения" | ||||
| "blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от страны назначения." | ||||
| "directConnectionsConfigsDesc" = "Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер." | ||||
|  | @ -413,19 +413,19 @@ | |||
| "ipv4Routing" = "Правила IPv4" | ||||
| "ipv4RoutingDesc" = "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4" | ||||
| "warpRouting" = "Правила WARP" | ||||
| "warpRoutingDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare" | ||||
| "warpRoutingDesc" = " Эти опции будут направлять трафик в зависимости от конкретного пункта назначения через WARP." | ||||
| "Template" = "Шаблон конфигурации Xray" | ||||
| "TemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона" | ||||
| "TemplateDesc" = "На основе шаблона создаётся конфигурационный файл Xray." | ||||
| "FreedomStrategy" = "Настройка стратегии протокола Freedom" | ||||
| "FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom" | ||||
| "RoutingStrategy" = "Настройка маршрутизации доменов" | ||||
| "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" | ||||
| "Torrent" = "Заблокировать BitTorrent" | ||||
| "Inbounds" = "Входящее соединение" | ||||
| "Inbounds" = "Инбаунды" | ||||
| "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" | ||||
| "Outbounds" = "Исходящее соединение" | ||||
| "Outbounds" = "Аутбаунды" | ||||
| "Balancers" = "Балансировщик" | ||||
| "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие соединения для этого сервера" | ||||
| "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера" | ||||
| "Routings" = "Маршрутизация" | ||||
| "RoutingsDesc" = "Важен приоритет каждого правила!" | ||||
| "completeTemplate" = "Все" | ||||
|  | @ -456,8 +456,8 @@ | |||
| "down" = "Опустить вниз" | ||||
| "source" = "Источник" | ||||
| "dest" = "Пункт назначения" | ||||
| "inbound" = "Входящее соединение" | ||||
| "outbound" = "Исходящее соединение" | ||||
| "inbound" = "Инбаунд" | ||||
| "outbound" = "Аутбаунд" | ||||
| "balancer" = "Балансировщик" | ||||
| "info" = "Информация" | ||||
| "add" = "Создать правило" | ||||
|  | @ -465,9 +465,9 @@ | |||
| "useComma" = "Элементы, разделённые запятыми" | ||||
| 
 | ||||
| [pages.xray.outbound] | ||||
| "addOutbound" = "Создать исходящее соединение" | ||||
| "addOutbound" = "Создать аутбаунд" | ||||
| "addReverse" = "Создать реверс-прокси" | ||||
| "editOutbound" = "Изменить исходящее соединение" | ||||
| "editOutbound" = "Изменить аутбаунд" | ||||
| "editReverse" = "Редактировать реверс-прокси" | ||||
| "tag" = "Тег" | ||||
| "tagDesc" = "Уникальный тег" | ||||
|  | @ -481,7 +481,7 @@ | |||
| "intercon" = "Соединение" | ||||
| "settings" = "Настройки" | ||||
| "accountInfo" = "Информация об учетной записи" | ||||
| "outboundStatus" = "Исходящий статус" | ||||
| "outboundStatus" = "Статус аутбаунда" | ||||
| "sendThrough" = "Отправить через" | ||||
| 
 | ||||
| [pages.xray.balancer] | ||||
|  | @ -494,7 +494,7 @@ | |||
| "balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag." | ||||
| 
 | ||||
| [pages.xray.wireguard] | ||||
| "secretKey" = "Приватный ключ" | ||||
| "secretKey" = "Секретный ключ" | ||||
| "publicKey" = "Публичный ключ" | ||||
| "allowedIPs" = "Разрешенные IP-адреса" | ||||
| "endpoint" = "Конечная точка" | ||||
|  | @ -555,8 +555,8 @@ | |||
| "modifyUser" = "Вы успешно изменили учетные данные администратора." | ||||
| "originalUserPassIncorrect" = "Неверное имя пользователя или пароль" | ||||
| "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" | ||||
| "getOutboundTrafficError" = "Ошибка получения исходящего трафика" | ||||
| "resetOutboundTrafficError" = "Ошибка сброса исходящего трафика" | ||||
| "getOutboundTrafficError" = "Ошибка получения трафика аутбаунда" | ||||
| "resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда" | ||||
| 
 | ||||
| [tgbot] | ||||
| "keyboardClosed" = "❌ Клавиатура закрыта." | ||||
|  | @ -564,7 +564,7 @@ | |||
| "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." | ||||
| "wentWrong" = "❌ Что-то пошло не так..." | ||||
| "noIpRecord" = "❗ Нет записей об IP-адресе." | ||||
| "noInbounds" = "❗ У вас не настроено ни одного подключения." | ||||
| "noInbounds" = "❗ У вас не настроено ни одного инбаунда." | ||||
| "unlimited" = "♾ Безлимит" | ||||
| "add" = "Добавить" | ||||
| "month" = "Месяц" | ||||
|  | @ -573,7 +573,7 @@ | |||
| "days" = "Дней" | ||||
| "hours" = "Часов" | ||||
| "unknown" = "Неизвестно" | ||||
| "inbounds" = "Подключения" | ||||
| "inbounds" = "Инбаунды" | ||||
| "clients" = "Клиенты" | ||||
| "offline" = "🔴 Офлайн" | ||||
| "online" = "🟢 Онлайн" | ||||
|  | @ -587,7 +587,7 @@ | |||
| "status" = "✅ Бот функционирует нормально." | ||||
| "usage" = "❗ Пожалуйста, укажите email для поиска." | ||||
| "getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>" | ||||
| "helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>" | ||||
| "helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>" | ||||
| "helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>" | ||||
| "restartUsage" = "\r\n\r\n<code>/restart</code>" | ||||
| "restartSuccess" = "✅ Ядро Xray успешно перезапущено." | ||||
|  | @ -653,8 +653,8 @@ | |||
| "pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." | ||||
| "email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." | ||||
| "comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." | ||||
| "inbound_client_data_id" = "🔄 Подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в подключение!" | ||||
| "inbound_client_data_pass" = "🔄 Подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в подключение!" | ||||
| "inbound_client_data_id" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" | ||||
| "inbound_client_data_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" | ||||
| "cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" | ||||
| "error_add_client"  = "⚠️ Ошибка:\n\n {{ .error }}" | ||||
| "using_default_value"  = "Используется значение по умолчанию👌" | ||||
|  | @ -676,7 +676,7 @@ | |||
| "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" | ||||
| "dbBackup" = "📂 Бэкап БД" | ||||
| "serverUsage" = "💻 Состояние сервера" | ||||
| "getInbounds" = "🔌 Подключения" | ||||
| "getInbounds" = "🔌 Инбаунды" | ||||
| "depleteSoon" = "⚠️ Скоро конец" | ||||
| "clientUsage" = "Статистика клиента" | ||||
| "onlines" = "🟢 Онлайн" | ||||
|  | @ -714,7 +714,7 @@ | |||
| [tgbot.answers] | ||||
| "successfulOperation" = "✅ Успешно!" | ||||
| "errorOperation" = "❗ Ошибка в операции." | ||||
| "getInboundsFailed" = "❌ Не удалось получить подключения." | ||||
| "getInboundsFailed" = "❌ Не удалось получить инбаунды." | ||||
| "getClientsFailed" = "❌ Не удалось получить клиентов." | ||||
| "canceled" = "❌ {{ .Email }}: Операция отменена." | ||||
| "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." | ||||
|  | @ -731,5 +731,5 @@ | |||
| "enableSuccess" = "✅ {{ .Email }}: Включено успешно." | ||||
| "disableSuccess" = "✅ {{ .Email }}: Отключено успешно." | ||||
| "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>" | ||||
| "chooseClient" = "Выберите клиента для подключения {{ .Inbound }}" | ||||
| "chooseInbound" = "Выберите подключение" | ||||
| "chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}" | ||||
| "chooseInbound" = "Выберите инбаунд" | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue