Merge branch 'main' into main

This commit is contained in:
Torotin 2025-06-25 05:24:00 +03:00 committed by GitHub
commit f7a3ebf2f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 2785 additions and 2542 deletions

View file

@ -2150,7 +2150,7 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings { Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol, constructor(protocol,
method = SSMethods.BLAKE3_AES_256_GCM, method = SSMethods.BLAKE3_AES_256_GCM,
password = RandomUtil.randomShadowsocksPassword(), password = '',
network = 'tcp,udp', network = 'tcp,udp',
shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()], shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()],
ivCheck = false, ivCheck = false,
@ -2188,7 +2188,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor( constructor(
method = '', method = '',
password = RandomUtil.randomShadowsocksPassword(), password = '',
email = RandomUtil.randomLowerAndNum(8), email = RandomUtil.randomLowerAndNum(8),
limitIp = 0, limitIp = 0,
totalGB = 0, totalGB = 0,

View file

@ -138,8 +138,14 @@ class RandomUtil {
} }
} }
static randomShadowsocksPassword() { static randomShadowsocksPassword(method = SSMethods.BLAKE3_AES_256_GCM) {
const array = new Uint8Array(32); let length = 32;
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
length = 16;
}
const array = new Uint8Array(length);
window.crypto.getRandomValues(array); window.crypto.getRandomValues(array);
@ -789,6 +795,25 @@ class LanguageManager {
if (window.navigator) { if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage; 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)) { if (LanguageManager.isSupportLanguage(lang)) {
CookieManager.setCookie("lang", lang, 150); CookieManager.setCookie("lang", lang, 150);
} else { } else {

View file

@ -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}}

View file

@ -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
View 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 }}

View file

@ -41,6 +41,7 @@
</template> </template>
</template> </template>
<template slot="client" slot-scope="text, client"> <template slot="client" slot-scope="text, client">
<a-space direction="horizontal" :size="2">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template> <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 v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template> </template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge> <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>
<template slot="traffic" slot-scope="text, client"> <template slot="traffic" slot-scope="text, client">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">

View file

@ -22,7 +22,7 @@
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
{{ i18n "password" }} {{ 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-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</template> </template>

View file

@ -31,7 +31,7 @@
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <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> </a-tooltip>
</template> </template>
<a-input v-model.trim="inbound.settings.password"></a-input> <a-input v-model.trim="inbound.settings.password"></a-input>

View file

@ -1,6 +1,4 @@
<!DOCTYPE html> {{ template "page/head_start" .}}
<html lang="en">
{{template "head" .}}
<style> <style>
.ant-table:not(.ant-table-expanded-row .ant-table) { .ant-table:not(.ant-table-expanded-row .ant-table) {
outline: 1px solid #f0f0f0; outline: 1px solid #f0f0f0;
@ -79,6 +77,19 @@
max-width: 200px; max-width: 200px;
overflow: hidden; 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 { .online-animation .ant-badge-status-dot {
animation: onlineAnimation 1.2s linear infinite; animation: onlineAnimation 1.2s linear infinite;
} }
@ -130,15 +141,16 @@
padding: 12px 2px; padding: 12px 2px;
} }
</style> </style>
{{ template "page/head_end" .}}
<body> {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <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> <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" }}' message='{{ i18n "secAlertTitle" }}'
color="red" color="red"
description='{{ i18n "secAlertSsl" }}' description='{{ i18n "secAlertSsl" }}'
@ -146,6 +158,13 @@
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <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-card size="small" :style="{ padding: '16px' }" hoverable>
<a-row> <a-row>
<a-col :sm="12" :md="6"> <a-col :sm="12" :md="6">
@ -208,8 +227,8 @@
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</transition> </a-col>
<transition name="list" appear> <a-col>
<a-card hoverable> <a-card hoverable>
<template #title> <template #title>
<a-space direction="horizontal"> <a-space direction="horizontal">
@ -382,25 +401,57 @@
<a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> <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>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </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-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>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </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-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>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </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-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> </a-popover>
@ -479,25 +530,57 @@
<a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> <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>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </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-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>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </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-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>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <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> </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-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> </a-popover>
@ -575,12 +658,14 @@
</a-table> </a-table>
</a-space> </a-space>
</a-card> </a-card>
</a-col>
</a-row>
</transition> </transition>
</a-spin> </a-spin>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</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/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.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> <script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
@ -589,6 +674,13 @@
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}} {{template "component/aCustomStatistic" .}}
{{template "component/aPersianDatepicker" .}} {{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> <script>
const columns = [{ const columns = [{
title: "ID", title: "ID",
@ -683,7 +775,10 @@
data: { data: {
themeSwitcher, themeSwitcher,
persianDatepicker, persianDatepicker,
spinning: false, loadingStates: {
fetched: false,
spinning: false
},
inbounds: [], inbounds: [],
dbInbounds: [], dbInbounds: [],
searchKey: '', searchKey: '',
@ -714,7 +809,18 @@
}, },
methods: { methods: {
loading(spinning = true) { 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() { async getDBInbounds() {
this.refreshing = true; this.refreshing = true;
@ -725,6 +831,7 @@
} }
await this.getOnlineUsers(); await this.getOnlineUsers();
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
setTimeout(() => { setTimeout(() => {
this.refreshing = false; this.refreshing = false;
@ -776,6 +883,9 @@
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound); this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
} }
} }
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true
}
if(this.enableFilter){ if(this.enableFilter){
this.filterInbounds(); this.filterInbounds();
} else { } else {
@ -1447,9 +1557,9 @@
}, },
async manualRefresh() { async manualRefresh() {
if (!this.refreshing) { if (!this.refreshing) {
this.spinning = true; this.loadingStates.spinning = true;
await this.getDBInbounds(); await this.getDBInbounds();
this.spinning = false; this.loadingStates.spinning = false;
} }
}, },
pagination(obj){ pagination(obj){
@ -1519,13 +1629,4 @@
}, },
}); });
</script> </script>
{{ template "page/body_end" .}}
{{template "modals/inboundModal"}}
{{template "modals/promptModal"}}
{{template "modals/qrcodeModal"}}
{{template "modals/textModal"}}
{{template "modals/inboundInfoModal"}}
{{template "modals/clientsModal"}}
{{template "modals/clientsBulkModal"}}
</body>
</html>

View file

@ -1,20 +1,9 @@
<!DOCTYPE html> {{ template "page/head_start" .}}
<html lang="en">
{{template "head" .}}
<style> <style>
@media (min-width: 769px) { @media (min-width: 769px) {
.ant-layout-content { .ant-layout-content {
margin: 24px 16px; 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 { .ant-card-dark h2 {
color: var(--dark-color-text-primary); color: var(--dark-color-text-primary);
@ -79,15 +68,16 @@
} }
} }
</style> </style>
{{ template "page/head_end" .}}
<body> {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"> <a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
<transition name="list" appear> <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" }}' message='{{ i18n "secAlertTitle" }}'
color="red" color="red"
description='{{ i18n "secAlertSsl" }}' description='{{ i18n "secAlertSsl" }}'
@ -96,15 +86,15 @@
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<template> <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-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
<a-row v-else> <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
<a-row> <a-col>
<a-card hoverable> <a-card hoverable>
<a-row> <a-row :gutter="[0, isMobile ? 16 : 0]">
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" :style="{ textAlign: 'center' }"> <a-col :span="12" :style="{ textAlign: 'center' }">
@ -154,7 +144,7 @@
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</a-row> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :lg="12">
<a-card hoverable> <a-card hoverable>
<template #title> <template #title>
@ -237,7 +227,7 @@
</a-tag> </a-tag>
</a> </a>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank"> <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> <span>{{ i18n "pages.index.documentation" }}</span>
</a-tag> </a-tag>
</a> </a>
@ -455,8 +445,8 @@
</a-list-item> </a-list-item>
</a-list> </a-list>
</a-modal> </a-modal>
</a-layout> </a-layout>
{{template "js" .}} {{template "page/body_scripts" .}}
{{template "component/aSidebar" .}} {{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}} {{template "component/aCustomStatistic" .}}
@ -489,7 +479,7 @@
} }
class Status { class Status {
constructor(data, isLoaded = false) { constructor(data) {
this.cpu = new CurTotal(0, 0); this.cpu = new CurTotal(0, 0);
this.cpuCores = 0; this.cpuCores = 0;
this.logicalPro = 0; this.logicalPro = 0;
@ -513,7 +503,6 @@
return; return;
} }
this.isLoaded = isLoaded;
this.cpu = new CurTotal(data.cpu, 100); this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores; this.cpuCores = data.cpuCores;
this.logicalPro = data.logicalPro; this.logicalPro = data.logicalPro;
@ -633,32 +622,39 @@
mixins: [MediaQueryMixin], mixins: [MediaQueryMixin],
data: { data: {
themeSwitcher, themeSwitcher,
loadingStates: {
fetched: false,
spinning: false
},
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
backupModal, backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false, showAlert: false,
showIp: false showIp: false
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
this.spinning = spinning; this.loadingStates.spinning = spinning;
this.loadingTip = tip; this.loadingTip = tip;
}, },
async getStatus() { async getStatus() {
try { try {
const msg = await HttpUtil.post('/server/status'); const msg = await HttpUtil.post('/server/status');
if (msg.success) { if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true;
}
this.setStatus(msg.obj, true); this.setStatus(msg.obj, true);
} }
} catch (e) { } catch (e) {
console.error("Failed to get status:", e); console.error("Failed to get status:", e);
} }
}, },
setStatus(data, isLoaded = false) { setStatus(data) {
this.status = new Status(data, isLoaded); this.status = new Status(data);
}, },
async openSelectV2rayVersion() { async openSelectV2rayVersion() {
this.loading(true); this.loading(true);
@ -788,5 +784,4 @@
}, },
}); });
</script> </script>
</body> {{ template "page/body_end" .}}
</html>

View file

@ -1,6 +1,4 @@
<!DOCTYPE html> {{ template "page/head_start" .}}
<html lang="en">
{{template "head" .}}
<style> <style>
html * { html * {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -453,9 +451,10 @@
margin: 2px 0 4px; margin: 2px 0 4px;
} }
</style> </style>
{{ template "page/head_end" .}}
<body> {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content class="under" :style="{ minHeight: '0' }"> <a-layout-content class="under" :style="{ minHeight: '0' }">
<div class="waves-header"> <div class="waves-header">
@ -542,10 +541,10 @@
</a-row> </a-row>
</a-layout-content> </a-layout-content>
</transition> </transition>
</a-layout> </a-layout>
{{template "js" .}} {{template "page/body_scripts" .}}
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}
<script> <script>
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
@ -621,6 +620,5 @@
newWord.classList.add('is-visible'); newWord.classList.add('is-visible');
} }
}); });
</script> </script>
</body> {{ template "page/body_end" .}}
</html>

View file

@ -1,6 +1,4 @@
<!DOCTYPE html> {{ template "page/head_start" .}}
<html lang="en">
{{template "head" .}}
<style> <style>
@media (min-width: 769px) { @media (min-width: 769px) {
.ant-layout-content { .ant-layout-content {
@ -60,14 +58,16 @@
margin-block-end: 12px; margin-block-end: 12px;
} }
</style> </style>
<body> {{ template "page/head_end" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <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> <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" }}' message='{{ i18n "secAlertTitle" }}'
color="red" color="red"
show-icon closable> show-icon closable>
@ -77,8 +77,16 @@
</template> </template>
</a-alert> </a-alert>
</transition> </transition>
<a-space direction="vertical"> <transition name="list" appear>
<a-card hoverable :style="{ marginBottom: '.5rem', overflowX: 'hidden' }"> <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-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal"> <a-space direction="horizontal">
@ -99,29 +107,54 @@
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</a-col>
<a-col>
<a-tabs default-active-key="1"> <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" . }} {{ template "settings/panel/general" . }}
</a-tab-pane> </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" . }} {{ template "settings/panel/security" . }}
</a-tab-pane> </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" . }} {{ template "settings/panel/telegram" . }}
</a-tab-pane> </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" . }} {{ template "settings/panel/subscription/general" . }}
</a-tab-pane> </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" . }} {{ template "settings/panel/subscription/json" . }}
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-space> </a-col>
</a-row>
</template>
</transition>
</a-spin> </a-spin>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</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/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/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
@ -132,10 +165,14 @@
<script> <script>
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
mixins: [MediaQueryMixin],
el: '#app', el: '#app',
data: { data: {
themeSwitcher, themeSwitcher,
spinning: false, loadingStates: {
fetched: false,
spinning: false
},
oldAllSetting: new AllSetting(), oldAllSetting: new AllSetting(),
allSetting: new AllSetting(), allSetting: new AllSetting(),
saveBtnDisable: true, saveBtnDisable: true,
@ -248,13 +285,16 @@
}, },
methods: { methods: {
loading(spinning = true) { loading(spinning = true) {
this.spinning = spinning; this.loadingStates.spinning = spinning;
}, },
async getAllSetting() { async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/all"); const msg = await HttpUtil.post("/panel/setting/all");
this.loading(false);
if (msg.success) { if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true
}
this.oldAllSetting = new AllSetting(msg.obj); this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj); this.allSetting = new AllSetting(msg.obj);
app.changeRemarkSample(); app.changeRemarkSample();
@ -508,7 +548,7 @@
if (!this.allSetting) return []; if (!this.allSetting) return [];
var alerts = [] var alerts = []
if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}'); 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 panelPath = window.location.pathname.split('/').length < 4
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}'); if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
if (this.allSetting.subEnable) { if (this.allSetting.subEnable) {
@ -531,5 +571,4 @@
} }
}); });
</script> </script>
</body> {{ template "page/body_end" .}}
</html>

View file

@ -1,5 +1,5 @@
{{define "settings/xray/advanced"}} {{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"}}' <a-list-item-meta title='{{ i18n "pages.xray.Template"}}'
description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta> description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" :style="{ margin: '10px 0' }" <a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" :style="{ margin: '10px 0' }"

View file

@ -1,74 +1,75 @@
<!DOCTYPE html> {{ template "page/head_start" .}}
<html lang="en">
{{template "head" .}}
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}"> <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/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css"> <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> <style>
@media (min-width: 769px) { @media (min-width: 769px) {
.ant-layout-content { .ant-layout-content {
margin: 24px 16px; margin: 24px 16px;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.ant-tabs-nav .ant-tabs-tab { .ant-tabs-nav .ant-tabs-tab {
margin: 0; margin: 0;
padding: 12px .5rem; padding: 12px .5rem;
} }
.ant-table-thead>tr>th, .ant-table-thead>tr>th,
.ant-table-tbody>tr>td { .ant-table-tbody>tr>td {
padding: 10px 0px; padding: 10px 0px;
} }
} }
.ant-tabs-bar { .ant-tabs-bar {
margin: 0; margin: 0;
} }
.ant-list-item { .ant-list-item {
display: block; display: block;
} }
.ant-list-item>li { .ant-list-item>li {
padding: 10px 20px !important; padding: 10px 20px !important;
} }
.ant-collapse-content-box .ant-alert { .ant-collapse-content-box .ant-alert {
margin-block-end: 12px; margin-block-end: 12px;
} }
</style> </style>
<body> {{ template "page/head_end" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar> <a-sidebar></a-sidebar>
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <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> <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" }}'
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable>
</a-alert> </a-alert>
</transition> </transition>
<a-space direction="vertical"> <transition name="list" appear>
<a-card hoverable :style="{ marginBottom: '.5rem' }"> <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-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.xray.save" }}</a-button> <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">{{ i18n "pages.xray.restart" }}</a-button> {{ i18n "pages.xray.save" }}
<a-popover v-if="restartResult" </a-button>
:overlay-class-name="themeSwitcher.currentTheme"> <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> <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
<template slot="content"> <template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span> <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span>
@ -80,45 +81,89 @@
<a-col :xs="24" :sm="14"> <a-col :xs="24" :sm="14">
<template> <template>
<div> <div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top> <a-back-top :target="() => document.getElementById('content-layout')"
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }" message='{{ i18n "pages.settings.infoDesc" }}' show-icon> 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> </a-alert>
</div> </div>
</template> </template>
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
<a-tabs class="ant-card-dark-box-nohover" default-active-key="1" </a-col>
@change="(activeKey) => { this.changePage(activeKey); }" <a-col>
<a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }"
:class="themeSwitcher.currentTheme"> :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" . }} {{ template "settings/xray/basics" . }}
</a-tab-pane> </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" . }} {{ template "settings/xray/routing" . }}
</a-tab-pane> </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" . }} {{ template "settings/xray/outbounds" . }}
</a-tab-pane> </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" . }} {{ template "settings/xray/reverse" . }}
</a-tab-pane> </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" . }} {{ template "settings/xray/balancers" . }}
</a-tab-pane> </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" . }} {{ template "settings/xray/dns" . }}
</a-tab-pane> </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" . }} {{ template "settings/xray/advanced" . }}
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-space> </a-col>
</a-row>
</transition>
</a-spin> </a-spin>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</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/aSidebar" .}}
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}
{{template "component/aTableSortable" .}} {{template "component/aTableSortable" .}}
@ -134,20 +179,28 @@
<script> <script>
const rulesColumns = [ const rulesColumns = [
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } }, { 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: '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.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }]
{ title: '{{ i18n "pages.inbounds.network"}}', children: [ },
{
title: '{{ i18n "pages.inbounds.network"}}', children: [
{ title: 'L4', dataIndex: 'network', align: 'center', width: 10 }, { title: 'L4', dataIndex: 'network', align: 'center', width: 10 },
{ title: '{{ i18n "protocol" }}', dataIndex: 'protocol', align: 'center', width: 15, ellipsis: true }, { title: '{{ i18n "protocol" }}', dataIndex: 'protocol', align: 'center', width: 15, ellipsis: true },
{ title: 'Attrs', dataIndex: 'attrs', align: 'center', width: 10, ellipsis: true } ]}, { title: 'Attrs', dataIndex: 'attrs', align: 'center', width: 10, ellipsis: true }]
{ title: '{{ i18n "pages.xray.rules.dest"}}', children: [ },
{
title: '{{ i18n "pages.xray.rules.dest"}}', children: [
{ title: 'IP', dataIndex: 'ip', align: 'center', width: 20, ellipsis: true }, { 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.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.inbounds.port" }}', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]
{ title: '{{ i18n "pages.xray.rules.inbound"}}', children: [ },
{
title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
{ title: '{{ i18n "pages.xray.outbound.tag" }}', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true }, { 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.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 17 },
{ title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 }, { title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },
]; ];
@ -177,8 +230,8 @@
const balancerColumns = [ const balancerColumns = [
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } }, { title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 }, { title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
{ title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }}, { title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' } },
{ title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }}, { title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' } },
]; ];
const dnsColumns = [ const dnsColumns = [
@ -201,7 +254,10 @@
data: { data: {
themeSwitcher, themeSwitcher,
isDarkTheme: themeSwitcher.isDarkTheme, isDarkTheme: themeSwitcher.isDarkTheme,
spinning: false, loadingStates: {
fetched: false,
spinning: false
},
oldXraySetting: '', oldXraySetting: '',
xraySetting: '', xraySetting: '',
inboundTags: [], inboundTags: [],
@ -330,7 +386,7 @@
}, },
methods: { methods: {
loading(spinning = true) { loading(spinning = true) {
this.spinning = spinning; this.loadingStates.spinning = spinning;
}, },
async getOutboundsTraffic() { async getOutboundsTraffic() {
const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic"); const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
@ -339,10 +395,13 @@
} }
}, },
async getXraySetting() { async getXraySetting() {
this.loading(true);
const msg = await HttpUtil.post("/panel/xray/"); const msg = await HttpUtil.post("/panel/xray/");
this.loading(false);
if (msg.success) { if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true
}
result = JSON.parse(msg.obj); result = JSON.parse(msg.obj);
xs = JSON.stringify(result.xraySetting, null, 2); xs = JSON.stringify(result.xraySetting, null, 2);
this.oldXraySetting = xs; this.oldXraySetting = xs;
@ -353,7 +412,7 @@
}, },
async updateXraySetting() { async updateXraySetting() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("/panel/xray/update", {xraySetting : this.xraySetting}); const msg = await HttpUtil.post("/panel/xray/update", { xraySetting: this.xraySetting });
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
await this.getXraySetting(); await this.getXraySetting();
@ -372,8 +431,8 @@
async getXrayResult() { async getXrayResult() {
const msg = await HttpUtil.get("/panel/xray/getXrayResult"); const msg = await HttpUtil.get("/panel/xray/getXrayResult");
if (msg.success) { if (msg.success) {
this.restartResult=msg.obj; this.restartResult = msg.obj;
if(msg.obj.length > 1) Vue.prototype.$message.error(msg.obj); if (msg.obj.length > 1) Vue.prototype.$message.error(msg.obj);
} }
}, },
async resetXrayConfigToDefault() { async resetXrayConfigToDefault() {
@ -386,8 +445,8 @@
} }
}, },
changePage(pageKey) { changePage(pageKey) {
if(pageKey == 'tpl-advanced') this.changeCode(); if (pageKey == 'tpl-advanced') this.changeCode();
if(pageKey == 'tpl-balancer') this.changeObsCode(); if (pageKey == 'tpl-balancer') this.changeObsCode();
}, },
syncRulesWithOutbound(tag, setting) { syncRulesWithOutbound(tag, setting) {
const newTemplateSettings = this.templateSettings; const newTemplateSettings = this.templateSettings;
@ -458,13 +517,13 @@
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
}, },
changeCode() { changeCode() {
if(this.cm != null) { if (this.cm != null) {
this.cm.toTextArea(); this.cm.toTextArea();
} }
textAreaObj = document.getElementById('xraySetting'); textAreaObj = document.getElementById('xraySetting');
textAreaObj.value = this[this.advSettings]; textAreaObj.value = this[this.advSettings];
this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions); this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
this.cm.on('change',editor => { this.cm.on('change', editor => {
value = editor.getValue(); value = editor.getValue();
if (this.isJsonString(value)) { if (this.isJsonString(value)) {
this[this.advSettings] = value; this[this.advSettings] = value;
@ -472,19 +531,19 @@
}); });
}, },
changeObsCode() { changeObsCode() {
if(this.cm != null) { if (this.cm != null) {
this.cm.toTextArea(); this.cm.toTextArea();
} }
if (this.obsSettings == ''){ if (this.obsSettings == '') {
this.cm = null; this.cm = null;
return return
} }
textAreaObj = document.getElementById('obsSetting'); textAreaObj = document.getElementById('obsSetting');
textAreaObj.value = this[this.obsSettings]; textAreaObj.value = this[this.obsSettings];
this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions); this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
this.cm.on('change',editor => { this.cm.on('change', editor => {
value = editor.getValue(); value = editor.getValue();
if(this.isJsonString(value)){ if (this.isJsonString(value)) {
this[this.obsSettings] = value; this[this.obsSettings] = value;
} }
}); });
@ -507,7 +566,7 @@
}, },
findOutboundAddress(o) { findOutboundAddress(o) {
serverObj = null; serverObj = null;
switch(o.protocol){ switch (o.protocol) {
case Protocols.VMess: case Protocols.VMess:
case Protocols.VLESS: case Protocols.VLESS:
serverObj = o.settings.vnext; serverObj = o.settings.vnext;
@ -527,13 +586,13 @@
} }
return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null; return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;
}, },
addOutbound(){ addOutbound() {
outModal.show({ outModal.show({
title: '{{ i18n "pages.xray.outbound.addOutbound"}}', title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
okText: '{{ i18n "pages.xray.outbound.addOutbound" }}', okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
confirm: (outbound) => { confirm: (outbound) => {
outModal.loading(); outModal.loading();
if(outbound.tag.length > 0){ if (outbound.tag.length > 0) {
this.templateSettings.outbounds.push(outbound); this.templateSettings.outbounds.push(outbound);
this.outboundSettings = JSON.stringify(this.templateSettings.outbounds); this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
} }
@ -543,9 +602,9 @@
tags: this.templateSettings.outbounds.map(obj => obj.tag) tags: this.templateSettings.outbounds.map(obj => obj.tag)
}); });
}, },
editOutbound(index){ editOutbound(index) {
outModal.show({ outModal.show({
title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index+1), title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index + 1),
outbound: app.templateSettings.outbounds[index], outbound: app.templateSettings.outbounds[index],
confirm: (outbound) => { confirm: (outbound) => {
outModal.loading(); outModal.loading();
@ -554,30 +613,30 @@
outModal.close(); outModal.close();
}, },
isEdit: true, isEdit: true,
tags: this.outboundData.filter((o) => o.key != index ).map(obj => obj.tag) tags: this.outboundData.filter((o) => o.key != index).map(obj => obj.tag)
}); });
}, },
deleteOutbound(index){ deleteOutbound(index) {
outbounds = this.templateSettings.outbounds; outbounds = this.templateSettings.outbounds;
outbounds.splice(index,1); outbounds.splice(index, 1);
this.outboundSettings = JSON.stringify(outbounds); this.outboundSettings = JSON.stringify(outbounds);
}, },
setFirstOutbound(index){ setFirstOutbound(index) {
outbounds = this.templateSettings.outbounds; outbounds = this.templateSettings.outbounds;
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]); outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
this.outboundSettings = JSON.stringify(outbounds); this.outboundSettings = JSON.stringify(outbounds);
}, },
addReverse(){ addReverse() {
reverseModal.show({ reverseModal.show({
title: '{{ i18n "pages.xray.outbound.addReverse"}}', title: '{{ i18n "pages.xray.outbound.addReverse"}}',
okText: '{{ i18n "pages.xray.outbound.addReverse" }}', okText: '{{ i18n "pages.xray.outbound.addReverse" }}',
confirm: (reverse, rules) => { confirm: (reverse, rules) => {
reverseModal.loading(); reverseModal.loading();
if(reverse.tag.length > 0){ if (reverse.tag.length > 0) {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
if(newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {}; if (newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
if(newTemplateSettings.reverse[reverse.type+'s'] == undefined) newTemplateSettings.reverse[reverse.type+'s'] = []; if (newTemplateSettings.reverse[reverse.type + 's'] == undefined) newTemplateSettings.reverse[reverse.type + 's'] = [];
newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain }); newTemplateSettings.reverse[reverse.type + 's'].push({ tag: reverse.tag, domain: reverse.domain });
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
// Add related rules // Add related rules
@ -589,33 +648,33 @@
isEdit: false isEdit: false
}); });
}, },
editReverse(index){ editReverse(index) {
if(this.reverseData[index].type == "bridge") { if (this.reverseData[index].type == "bridge") {
oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag); oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag);
} else { } else {
oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag); oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag);
} }
reverseModal.show({ reverseModal.show({
title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index+1), title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index + 1),
reverse: this.reverseData[index], reverse: this.reverseData[index],
rules: oldRules, rules: oldRules,
confirm: (reverse, rules) => { confirm: (reverse, rules) => {
reverseModal.loading(); reverseModal.loading();
if(reverse.tag.length > 0){ if (reverse.tag.length > 0) {
oldData = this.reverseData[index]; oldData = this.reverseData[index];
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
oldReverseIndex = newTemplateSettings.reverse[oldData.type+'s'].findIndex(rs => rs.tag == oldData.tag); oldReverseIndex = newTemplateSettings.reverse[oldData.type + 's'].findIndex(rs => rs.tag == oldData.tag);
oldRuleIndex0 = oldRules.length>0 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[0])) : -1; oldRuleIndex0 = oldRules.length > 0 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[0])) : -1;
oldRuleIndex1 = oldRules.length==2 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[1])) : -1; oldRuleIndex1 = oldRules.length == 2 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[1])) : -1;
if(oldData.type == reverse.type){ if (oldData.type == reverse.type) {
newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = { tag: reverse.tag, domain: reverse.domain }; newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = { tag: reverse.tag, domain: reverse.domain };
} else { } else {
newTemplateSettings.reverse[oldData.type+'s'].splice(oldReverseIndex,1); newTemplateSettings.reverse[oldData.type + 's'].splice(oldReverseIndex, 1);
// delete empty object // delete empty object
if(newTemplateSettings.reverse[oldData.type+'s'].length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s'); if (newTemplateSettings.reverse[oldData.type + 's'].length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type + 's');
// add other type of reverse if it is not exist // add other type of reverse if it is not exist
if(!newTemplateSettings.reverse[reverse.type+'s']) newTemplateSettings.reverse[reverse.type+'s'] = []; if (!newTemplateSettings.reverse[reverse.type + 's']) newTemplateSettings.reverse[reverse.type + 's'] = [];
newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain }); newTemplateSettings.reverse[reverse.type + 's'].push({ tag: reverse.tag, domain: reverse.domain });
} }
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
@ -630,22 +689,22 @@
isEdit: true isEdit: true
}); });
}, },
deleteReverse(index){ deleteReverse(index) {
oldData = this.reverseData[index]; oldData = this.reverseData[index];
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
reverseTypeObj = newTemplateSettings.reverse[oldData.type+'s']; reverseTypeObj = newTemplateSettings.reverse[oldData.type + 's'];
realIndex = reverseTypeObj.findIndex(r => r.tag==oldData.tag && r.domain==oldData.domain); realIndex = reverseTypeObj.findIndex(r => r.tag == oldData.tag && r.domain == oldData.domain);
newTemplateSettings.reverse[oldData.type+'s'].splice(realIndex,1); newTemplateSettings.reverse[oldData.type + 's'].splice(realIndex, 1);
// delete empty objects // delete empty objects
if(reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s'); if (reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type + 's');
if(Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse'); if (Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse');
// delete related routing rules // delete related routing rules
newRules = newTemplateSettings.routing.rules; newRules = newTemplateSettings.routing.rules;
if(oldData.type == "bridge"){ if (oldData.type == "bridge") {
newRules = newTemplateSettings.routing.rules.filter(r => !( r.inboundTag && r.inboundTag.length == 1 && r.inboundTag[0] == oldData.tag)); newRules = newTemplateSettings.routing.rules.filter(r => !(r.inboundTag && r.inboundTag.length == 1 && r.inboundTag[0] == oldData.tag));
} else if(oldData.type == "portal"){ } else if (oldData.type == "portal") {
newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag); newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
} }
newTemplateSettings.routing.rules = newRules; newTemplateSettings.routing.rules = newRules;
@ -660,7 +719,7 @@
data = [] data = []
if (this.templateSettings != null) { if (this.templateSettings != null) {
this.templateSettings.outbounds.forEach((o, index) => { this.templateSettings.outbounds.forEach((o, index) => {
data.push({'key': index, ...o}); data.push({ 'key': index, ...o });
}); });
} }
@ -732,10 +791,10 @@
}; };
// Remove old tag // Remove old tag
if (newTemplateSettings.observatory){ if (newTemplateSettings.observatory) {
newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory.subjectSelector.filter(s => s != oldTag); newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory.subjectSelector.filter(s => s != oldTag);
} }
if (newTemplateSettings.burstObservatory){ if (newTemplateSettings.burstObservatory) {
newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory.subjectSelector.filter(s => s != oldTag); newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory.subjectSelector.filter(s => s != oldTag);
} }
@ -762,7 +821,7 @@
isEdit: true isEdit: true
}); });
}, },
updateObservatorySelectors(){ updateObservatorySelectors() {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
const leastPings = this.balancersData.filter((b) => b.strategy == 'leastPing'); const leastPings = this.balancersData.filter((b) => b.strategy == 'leastPing');
const leastLoads = this.balancersData.filter((b) => const leastLoads = this.balancersData.filter((b) =>
@ -770,7 +829,7 @@
b.strategy === 'roundRobin' || b.strategy === 'roundRobin' ||
b.strategy === 'random' b.strategy === 'random'
); );
if (leastPings.length>0){ if (leastPings.length > 0) {
if (!newTemplateSettings.observatory) if (!newTemplateSettings.observatory)
newTemplateSettings.observatory = this.defaultObservatory; newTemplateSettings.observatory = this.defaultObservatory;
newTemplateSettings.observatory.subjectSelector = []; newTemplateSettings.observatory.subjectSelector = [];
@ -783,7 +842,7 @@
} else { } else {
delete newTemplateSettings.observatory delete newTemplateSettings.observatory
} }
if (leastLoads.length>0){ if (leastLoads.length > 0) {
if (!newTemplateSettings.burstObservatory) if (!newTemplateSettings.burstObservatory)
newTemplateSettings.burstObservatory = this.defaultBurstObservatory; newTemplateSettings.burstObservatory = this.defaultBurstObservatory;
newTemplateSettings.burstObservatory.subjectSelector = []; newTemplateSettings.burstObservatory.subjectSelector = [];
@ -828,7 +887,7 @@
} }
}); });
}, },
addDNSServer(){ addDNSServer() {
dnsModal.show({ dnsModal.show({
title: '{{ i18n "pages.xray.dns.add" }}', title: '{{ i18n "pages.xray.dns.add" }}',
confirm: (dnsServer) => { confirm: (dnsServer) => {
@ -840,9 +899,9 @@
isEdit: false isEdit: false
}); });
}, },
editDNSServer(index){ editDNSServer(index) {
dnsModal.show({ dnsModal.show({
title: '{{ i18n "pages.xray.dns.edit" }} #' + (index+1), title: '{{ i18n "pages.xray.dns.edit" }} #' + (index + 1),
dnsServer: this.dnsServers[index], dnsServer: this.dnsServers[index],
confirm: (dnsServer) => { confirm: (dnsServer) => {
dnsServers = this.dnsServers; dnsServers = this.dnsServers;
@ -853,16 +912,16 @@
isEdit: true isEdit: true
}); });
}, },
deleteDNSServer(index){ deleteDNSServer(index) {
newDnsServers = this.dnsServers; newDnsServers = this.dnsServers;
newDnsServers.splice(index,1); newDnsServers.splice(index, 1);
this.dnsServers = newDnsServers; this.dnsServers = newDnsServers;
}, },
addFakedns() { addFakedns() {
fakednsModal.show({ fakednsModal.show({
title: '{{ i18n "pages.xray.fakedns.add" }}', title: '{{ i18n "pages.xray.fakedns.add" }}',
confirm: (item) => { confirm: (item) => {
fakeDns = this.fakeDns?? []; fakeDns = this.fakeDns ?? [];
fakeDns.push(item); fakeDns.push(item);
this.fakeDns = fakeDns; this.fakeDns = fakeDns;
fakednsModal.close(); fakednsModal.close();
@ -870,9 +929,9 @@
isEdit: false isEdit: false
}); });
}, },
editFakedns(index){ editFakedns(index) {
fakednsModal.show({ fakednsModal.show({
title: '{{ i18n "pages.xray.fakedns.edit" }} #' + (index+1), title: '{{ i18n "pages.xray.fakedns.edit" }} #' + (index + 1),
fakeDns: this.fakeDns[index], fakeDns: this.fakeDns[index],
confirm: (item) => { confirm: (item) => {
fakeDns = this.fakeDns; fakeDns = this.fakeDns;
@ -883,18 +942,18 @@
isEdit: true isEdit: true
}); });
}, },
deleteFakedns(index){ deleteFakedns(index) {
fakeDns = this.fakeDns; fakeDns = this.fakeDns;
fakeDns.splice(index,1); fakeDns.splice(index, 1);
this.fakeDns = fakeDns; this.fakeDns = fakeDns;
}, },
addRule(){ addRule() {
ruleModal.show({ ruleModal.show({
title: '{{ i18n "pages.xray.rules.add"}}', title: '{{ i18n "pages.xray.rules.add"}}',
okText: '{{ i18n "pages.xray.rules.add" }}', okText: '{{ i18n "pages.xray.rules.add" }}',
confirm: (rule) => { confirm: (rule) => {
ruleModal.loading(); ruleModal.loading();
if(JSON.stringify(rule).length > 3){ if (JSON.stringify(rule).length > 3) {
this.templateSettings.routing.rules.push(rule); this.templateSettings.routing.rules.push(rule);
this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules); this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
} }
@ -903,13 +962,13 @@
isEdit: false isEdit: false
}); });
}, },
editRule(index){ editRule(index) {
ruleModal.show({ ruleModal.show({
title: '{{ i18n "pages.xray.rules.edit"}} ' + (index+1), title: '{{ i18n "pages.xray.rules.edit"}} ' + (index + 1),
rule: app.templateSettings.routing.rules[index], rule: app.templateSettings.routing.rules[index],
confirm: (rule) => { confirm: (rule) => {
ruleModal.loading(); ruleModal.loading();
if(JSON.stringify(rule).length > 3){ if (JSON.stringify(rule).length > 3) {
this.templateSettings.routing.rules[index] = rule; this.templateSettings.routing.rules[index] = rule;
this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules); this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
} }
@ -918,18 +977,18 @@
isEdit: true isEdit: true
}); });
}, },
replaceRule(old_index,new_index){ replaceRule(old_index, new_index) {
rules = this.templateSettings.routing.rules; rules = this.templateSettings.routing.rules;
if (new_index >= rules.length) rules.push(undefined); if (new_index >= rules.length) rules.push(undefined);
rules.splice(new_index, 0, rules.splice(old_index, 1)[0]); rules.splice(new_index, 0, rules.splice(old_index, 1)[0]);
this.routingRuleSettings = JSON.stringify(rules); this.routingRuleSettings = JSON.stringify(rules);
}, },
deleteRule(index){ deleteRule(index) {
rules = this.templateSettings.routing.rules; rules = this.templateSettings.routing.rules;
rules.splice(index,1); rules.splice(index, 1);
this.routingRuleSettings = JSON.stringify(rules); this.routingRuleSettings = JSON.stringify(rules);
}, },
showWarp(){ showWarp() {
warpModal.show(); warpModal.show();
} }
}, },
@ -978,7 +1037,7 @@
data = [] data = []
if (this.templateSettings != null) { if (this.templateSettings != null) {
this.templateSettings.outbounds.forEach((o, index) => { this.templateSettings.outbounds.forEach((o, index) => {
data.push({'key': index, ...o}); data.push({ 'key': index, ...o });
}); });
} }
return data; return data;
@ -988,14 +1047,14 @@
get: function () { get: function () {
data = [] data = []
if (this.templateSettings != null && this.templateSettings.reverse != null) { if (this.templateSettings != null && this.templateSettings.reverse != null) {
if(this.templateSettings.reverse.bridges) { if (this.templateSettings.reverse.bridges) {
this.templateSettings.reverse.bridges.forEach((o, index) => { this.templateSettings.reverse.bridges.forEach((o, index) => {
data.push({'key': index, 'type':'bridge', ...o}); data.push({ 'key': index, 'type': 'bridge', ...o });
}); });
} }
if(this.templateSettings.reverse.portals){ if (this.templateSettings.reverse.portals) {
this.templateSettings.reverse.portals.forEach((o, index) => { this.templateSettings.reverse.portals.forEach((o, index) => {
data.push({'key': index, 'type':'portal', ...o}); data.push({ 'key': index, 'type': 'portal', ...o });
}); });
} }
} }
@ -1015,17 +1074,17 @@
data = []; data = [];
if (this.templateSettings != null) { if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach((r, index) => { this.templateSettings.routing.rules.forEach((r, index) => {
data.push({'key': index, ...r}); data.push({ 'key': index, ...r });
}); });
// Make rules readable // Make rules readable
data.forEach(r => { data.forEach(r => {
if(r.domain) r.domain = r.domain.join(',') if (r.domain) r.domain = r.domain.join(',')
if(r.ip) r.ip = r.ip.join(',') if (r.ip) r.ip = r.ip.join(',')
if(r.source) r.source = r.source.join(','); if (r.source) r.source = r.source.join(',');
if(r.user) r.user = r.user.join(',') if (r.user) r.user = r.user.join(',')
if(r.inboundTag) r.inboundTag = r.inboundTag.join(',') if (r.inboundTag) r.inboundTag = r.inboundTag.join(',')
if(r.protocol) r.protocol = r.protocol.join(',') if (r.protocol) r.protocol = r.protocol.join(',')
if(r.attrs) r.attrs = JSON.stringify(r.attrs, null, 2) if (r.attrs) r.attrs = JSON.stringify(r.attrs, null, 2)
}); });
} }
return data; return data;
@ -1041,7 +1100,7 @@
'tag': o.tag ? o.tag : "", 'tag': o.tag ? o.tag : "",
'strategy': o.strategy?.type ?? "random", 'strategy': o.strategy?.type ?? "random",
'selector': o.selector ? o.selector : [], 'selector': o.selector ? o.selector : [],
'fallbackTag': o.fallbackTag?? '', 'fallbackTag': o.fallbackTag ?? '',
}); });
}); });
} }
@ -1081,10 +1140,10 @@
set: function (newValue) { set: function (newValue) {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && o.tag == "direct"); freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && o.tag == "direct");
if(freedomOutboundIndex == -1){ if (freedomOutboundIndex == -1) {
newTemplateSettings.outbounds.push({protocol: "freedom", tag: "direct", settings: { "domainStrategy": newValue }}); newTemplateSettings.outbounds.push({ protocol: "freedom", tag: "direct", settings: { "domainStrategy": newValue } });
} else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) { } else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
newTemplateSettings.outbounds[freedomOutboundIndex].settings = {"domainStrategy": newValue}; newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
} else { } else {
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue; newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
} }
@ -1273,8 +1332,8 @@
}, },
}, },
WarpExist: { WarpExist: {
get: function() { get: function () {
return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp")>=0 : false; return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp") >= 0 : false;
}, },
}, },
enableDNS: { enableDNS: {
@ -1410,5 +1469,4 @@
}, },
}); });
</script> </script>
</body> {{ template "page/body_end" .}}
</html>

View file

@ -33,7 +33,7 @@
"sure" = "Да" "sure" = "Да"
"encryption" = "Шифрование" "encryption" = "Шифрование"
"useIPv4ForHost" = "Использовать IPv4 для хоста" "useIPv4ForHost" = "Использовать IPv4 для хоста"
"transmission" = "Протокол" "transmission" = "Транспорт"
"host" = "Хост" "host" = "Хост"
"path" = "Путь" "path" = "Путь"
"camouflage" = "Маскировка" "camouflage" = "Маскировка"
@ -59,10 +59,10 @@
"security" = "Безопасность" "security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности" "secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения" "secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения"
"secAlertConf" = "Некоторые настройки уязвимы для атак. Рекомендуем усилить протоколы безопасности, чтобы предотвратить проблемы в будущем." "secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту."
"secAlertSSL" = "Ваше подключение к панели небезопасно. Пожалуйста, установите SSL сертификат для защиты данных." "secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных."
"secAlertPanelPort" = "Порт, на котором работает панель небезопасен. Пожалуйста, установите случайный или просто другой порт." "secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт."
"secAlertPanelURI" = "URI-адрес панели по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." "secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным."
"secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." "secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес."
"secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес." "secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес."
"emptyDnsDesc" = "Нет добавленных DNS-серверов." "emptyDnsDesc" = "Нет добавленных DNS-серверов."
@ -75,8 +75,8 @@
"theme" = "Тема" "theme" = "Тема"
"dark" = "Темная" "dark" = "Темная"
"ultraDark" = "Очень темная" "ultraDark" = "Очень темная"
"dashboard" = "Статус системы" "dashboard" = "Дашборд"
"inbounds" = "Входящие подключения" "inbounds" = "Инбаунды"
"settings" = "Настройки" "settings" = "Настройки"
"xray" = "Настройки Xray" "xray" = "Настройки Xray"
"logout" = "Выход" "logout" = "Выход"
@ -91,15 +91,15 @@
"invalidFormData" = "Недопустимый формат данных" "invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя" "emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль" "emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или код двухфакторной аутентификации." "wrongUsernameOrPassword" = "Неверные данные учетной записи."
"successLogin" = "Успешный вход" "successLogin" = "Вы успешно вошли в аккаунт"
[pages.index] [pages.index]
"title" = "Статус системы" "title" = "Дашборд"
"cpu" = "ЦП" "cpu" = "ЦП"
"logicalProcessors" = "Логические процессоры" "logicalProcessors" = "Логические процессоры"
"frequency" = "Частота" "frequency" = "Частота"
"swap" = "Swap" "swap" = "Файл подкачки"
"storage" = "Диск" "storage" = "Диск"
"memory" = "ОЗУ" "memory" = "ОЗУ"
"threads" = "Потоки" "threads" = "Потоки"
@ -136,7 +136,7 @@
"geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofileUpdatePopover" = "Геофайл успешно обновлён" "geofileUpdatePopover" = "Геофайл успешно обновлён"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу" "dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Логи" "logs" = "Журнал"
"config" = "Конфигурация" "config" = "Конфигурация"
"backup" = "Резервная копия" "backup" = "Резервная копия"
"backupTitle" = "Резервная копия базы данных" "backupTitle" = "Резервная копия базы данных"
@ -151,10 +151,10 @@
"getConfigError" = "Произошла ошибка при получении конфигурационного файла" "getConfigError" = "Произошла ошибка при получении конфигурационного файла"
[pages.inbounds] [pages.inbounds]
"title" = "Входящие подключения" "title" = "Инбаунды"
"totalDownUp" = "Объем отправленного/полученного трафика" "totalDownUp" = "Объем отправленного/полученного трафика"
"totalUsage" = "Всего трафика" "totalUsage" = "Всего трафика"
"inboundCount" = "Всего подключений" "inboundCount" = "Всего инбаундов"
"operate" = "Меню" "operate" = "Меню"
"enable" = "Включить" "enable" = "Включить"
"remark" = "Примечание" "remark" = "Примечание"
@ -165,18 +165,18 @@
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"
"expireDate" = "Дата окончания" "expireDate" = "Дата окончания"
"resetTraffic" = "Сброс трафика" "resetTraffic" = "Сброс трафика"
"addInbound" = "Создать новое подключение" "addInbound" = "Создать инбаунд"
"generalActions" = "Общие действия" "generalActions" = "Общие действия"
"autoRefresh" = "Автообновление" "autoRefresh" = "Автообновление"
"autoRefreshInterval" = "Интервал" "autoRefreshInterval" = "Интервал"
"modifyInbound" = "Изменить входящее подключение" "modifyInbound" = "Изменить инбаунд"
"deleteInbound" = "Удалить входящее подключение" "deleteInbound" = "Удалить инбаунд"
"deleteInboundContent" = "Вы уверены, что хотите удалить входящее подключение?" "deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?"
"deleteClient" = "Удалить клиента" "deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?" "deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
"inboundUpdateSuccess" = "Входящее подключение успешно обновлено." "inboundUpdateSuccess" = "Инбаунд успешно обновлен."
"inboundCreateSuccess" = "Входящее подключение успешно создано." "inboundCreateSuccess" = "Инбаунд успешно создан."
"copyLink" = "Копировать ссылку" "copyLink" = "Копировать ссылку"
"address" = "Адрес" "address" = "Адрес"
"network" = "Сеть" "network" = "Сеть"
@ -196,11 +196,11 @@
"export" = "Экспорт ссылок" "export" = "Экспорт ссылок"
"clone" = "Клонировать" "clone" = "Клонировать"
"cloneInbound" = "Клонировать" "cloneInbound" = "Клонировать"
"cloneInboundContent" = "Будут клонированы все настройки входящих подключений, кроме списка клиентов, порта и IP-адреса прослушивания" "cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundOk" = "Клонировано" "cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Сброс трафика всех подключений" "resetAllTraffic" = "Сброс трафика всех инбаундов"
"resetAllTrafficTitle" = "Сброс трафика всех подключений" "resetAllTrafficTitle" = "Сброс трафика всех инбаундов"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?" "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?"
"resetInboundClientTraffics" = "Сброс трафика клиента" "resetInboundClientTraffics" = "Сброс трафика клиента"
"resetInboundClientTrafficTitle" = "Сброс трафика клиентов" "resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
@ -222,10 +222,10 @@
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'"
"info" = "Информация" "info" = "Информация"
"same" = "Тот же" "same" = "Тот же"
"inboundData" = "Данные подключений" "inboundData" = "Данные инбаундов"
"exportInbound" = "Экспорт входящих подключений" "exportInbound" = "Экспорт инбаундов"
"import" = "Импортировать" "import" = "Импортировать"
"importInbound" = "Импорт входящих подключений" "importInbound" = "Импорт инбаундов"
[pages.client] [pages.client]
"add" = "Создать клиента" "add" = "Создать клиента"
@ -249,13 +249,13 @@
"obtain" = "Получить" "obtain" = "Получить"
"updateSuccess" = "Обновление прошло успешно" "updateSuccess" = "Обновление прошло успешно"
"logCleanSuccess" = "Лог был очищен" "logCleanSuccess" = "Лог был очищен"
"inboundsUpdateSuccess" = "Входящие подключения успешно обновлены" "inboundsUpdateSuccess" = "Инбаунды успешно обновлены"
"inboundUpdateSuccess" = "Входящее подключение успешно обновлено" "inboundUpdateSuccess" = "Инбаунд успешно обновлено"
"inboundCreateSuccess" = "Входящее подключение успешно создано" "inboundCreateSuccess" = "Инбаунд успешно создано"
"inboundDeleteSuccess" = "Входящее подключение успешно удалено" "inboundDeleteSuccess" = "Инбаунд успешно удалено"
"inboundClientAddSuccess" = "Клиент(ы) входящего подключения добавлен(ы)" "inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)"
"inboundClientDeleteSuccess" = "Клиент входящего подключения удалён" "inboundClientDeleteSuccess" = "Клиент инбаунда удалён"
"inboundClientUpdateSuccess" = "Клиент входящего подключения обновлён" "inboundClientUpdateSuccess" = "Клиент инбаунда обновлён"
"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
"resetAllTrafficSuccess" = "Весь трафик сброшен" "resetAllTrafficSuccess" = "Весь трафик сброшен"
@ -281,15 +281,15 @@
[pages.settings] [pages.settings]
"title" = "Настройки" "title" = "Настройки"
"save" = "Сохранить" "save" = "Сохранить"
"infoDesc" = "Каждое выполненное изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу" "infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу."
"restartPanel" = "Перезапуск панели" "restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
"restartPanelSuccess" = "Панель успешно перезапущена" "restartPanelSuccess" = "Панель успешно перезапущена"
"actions" = "Действия" "actions" = "Действия"
"resetDefaultConfig" = "Восстановить настройки по умолчанию" "resetDefaultConfig" = "Восстановить настройки по умолчанию"
"panelSettings" = "Настройки панели" "panelSettings" = "Панель"
"securitySettings" = "Настройки безопасности" "securitySettings" = "Учетная запись"
"TGBotSettings" = "Настройки Telegram бота" "TGBotSettings" = "Telegram"
"panelListeningIP" = "IP-адрес для управления панелью" "panelListeningIP" = "IP-адрес для управления панелью"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен панели" "panelListeningDomain" = "Домен панели"
@ -303,7 +303,7 @@
"panelUrlPath" = "Корневой путь URL адреса панели" "panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
"pageSize" = "Размер нумерации страниц" "pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для таблицы входящих подключений. Установите 0, чтобы отключить" "pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения" "remarkModel" = "Модель примечания и символ разделения"
"datepicker" = "Выбор даты" "datepicker" = "Выбор даты"
"datepickerPlaceholder" = "Выберите дату" "datepickerPlaceholder" = "Выберите дату"
@ -391,7 +391,7 @@
[pages.xray] [pages.xray]
"title" = "Настройки Xray" "title" = "Настройки Xray"
"save" = "Сохранить" "save" = "Сохранить"
"restart" = "Перезапустить Xray" "restart" = "Перезапуск Xray"
"restartSuccess" = "Xray успешно перезапущен" "restartSuccess" = "Xray успешно перезапущен"
"stopSuccess" = "Xray успешно остановлен" "stopSuccess" = "Xray успешно остановлен"
"restartError" = "Произошла ошибка при перезапуске Xray." "restartError" = "Произошла ошибка при перезапуске Xray."
@ -402,7 +402,7 @@
"generalConfigsDesc" = "Эти параметры описывают общие настройки" "generalConfigsDesc" = "Эти параметры описывают общие настройки"
"logConfigs" = "Логи" "logConfigs" = "Логи"
"logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!" "logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!"
"blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам и веб-сайтами" "blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам"
"basicRouting" = "Базовые соединения" "basicRouting" = "Базовые соединения"
"blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от страны назначения." "blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от страны назначения."
"directConnectionsConfigsDesc" = "Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер." "directConnectionsConfigsDesc" = "Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер."
@ -413,19 +413,19 @@
"ipv4Routing" = "Правила IPv4" "ipv4Routing" = "Правила IPv4"
"ipv4RoutingDesc" = "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4" "ipv4RoutingDesc" = "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4"
"warpRouting" = "Правила WARP" "warpRouting" = "Правила WARP"
"warpRoutingDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare" "warpRoutingDesc" = " Эти опции будут направлять трафик в зависимости от конкретного пункта назначения через WARP."
"Template" = "Шаблон конфигурации Xray" "Template" = "Шаблон конфигурации Xray"
"TemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона" "TemplateDesc" = "На основе шаблона создаётся конфигурационный файл Xray."
"FreedomStrategy" = "Настройка стратегии протокола Freedom" "FreedomStrategy" = "Настройка стратегии протокола Freedom"
"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom" "FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom"
"RoutingStrategy" = "Настройка маршрутизации доменов" "RoutingStrategy" = "Настройка маршрутизации доменов"
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"Torrent" = "Заблокировать BitTorrent" "Torrent" = "Заблокировать BitTorrent"
"Inbounds" = "Входящее соединение" "Inbounds" = "Инбаунды"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
"Outbounds" = "Исходящее соединение" "Outbounds" = "Аутбаунды"
"Balancers" = "Балансировщик" "Balancers" = "Балансировщик"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие соединения для этого сервера" "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера"
"Routings" = "Маршрутизация" "Routings" = "Маршрутизация"
"RoutingsDesc" = "Важен приоритет каждого правила!" "RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все" "completeTemplate" = "Все"
@ -456,8 +456,8 @@
"down" = "Опустить вниз" "down" = "Опустить вниз"
"source" = "Источник" "source" = "Источник"
"dest" = "Пункт назначения" "dest" = "Пункт назначения"
"inbound" = "Входящее соединение" "inbound" = "Инбаунд"
"outbound" = "Исходящее соединение" "outbound" = "Аутбаунд"
"balancer" = "Балансировщик" "balancer" = "Балансировщик"
"info" = "Информация" "info" = "Информация"
"add" = "Создать правило" "add" = "Создать правило"
@ -465,9 +465,9 @@
"useComma" = "Элементы, разделённые запятыми" "useComma" = "Элементы, разделённые запятыми"
[pages.xray.outbound] [pages.xray.outbound]
"addOutbound" = "Создать исходящее соединение" "addOutbound" = "Создать аутбаунд"
"addReverse" = "Создать реверс-прокси" "addReverse" = "Создать реверс-прокси"
"editOutbound" = "Изменить исходящее соединение" "editOutbound" = "Изменить аутбаунд"
"editReverse" = "Редактировать реверс-прокси" "editReverse" = "Редактировать реверс-прокси"
"tag" = "Тег" "tag" = "Тег"
"tagDesc" = "Уникальный тег" "tagDesc" = "Уникальный тег"
@ -481,7 +481,7 @@
"intercon" = "Соединение" "intercon" = "Соединение"
"settings" = "Настройки" "settings" = "Настройки"
"accountInfo" = "Информация об учетной записи" "accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Исходящий статус" "outboundStatus" = "Статус аутбаунда"
"sendThrough" = "Отправить через" "sendThrough" = "Отправить через"
[pages.xray.balancer] [pages.xray.balancer]
@ -494,7 +494,7 @@
"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag." "balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "Приватный ключ" "secretKey" = "Секретный ключ"
"publicKey" = "Публичный ключ" "publicKey" = "Публичный ключ"
"allowedIPs" = "Разрешенные IP-адреса" "allowedIPs" = "Разрешенные IP-адреса"
"endpoint" = "Конечная точка" "endpoint" = "Конечная точка"
@ -555,8 +555,8 @@
"modifyUser" = "Вы успешно изменили учетные данные администратора." "modifyUser" = "Вы успешно изменили учетные данные администратора."
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
"getOutboundTrafficError" = "Ошибка получения исходящего трафика" "getOutboundTrafficError" = "Ошибка получения трафика аутбаунда"
"resetOutboundTrafficError" = "Ошибка сброса исходящего трафика" "resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Клавиатура закрыта." "keyboardClosed" = "❌ Клавиатура закрыта."
@ -564,7 +564,7 @@
"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
"wentWrong" = "❌ Что-то пошло не так..." "wentWrong" = "❌ Что-то пошло не так..."
"noIpRecord" = "❗ Нет записей об IP-адресе." "noIpRecord" = "❗ Нет записей об IP-адресе."
"noInbounds" = "❗ У вас не настроено ни одного подключения." "noInbounds" = "❗ У вас не настроено ни одного инбаунда."
"unlimited" = "♾ Безлимит" "unlimited" = "♾ Безлимит"
"add" = "Добавить" "add" = "Добавить"
"month" = "Месяц" "month" = "Месяц"
@ -573,7 +573,7 @@
"days" = "Дней" "days" = "Дней"
"hours" = "Часов" "hours" = "Часов"
"unknown" = "Неизвестно" "unknown" = "Неизвестно"
"inbounds" = "Подключения" "inbounds" = "Инбаунды"
"clients" = "Клиенты" "clients" = "Клиенты"
"offline" = "🔴 Офлайн" "offline" = "🔴 Офлайн"
"online" = "🟢 Онлайн" "online" = "🟢 Онлайн"
@ -587,7 +587,7 @@
"status" = "✅ Бот функционирует нормально." "status" = "✅ Бот функционирует нормально."
"usage" = "❗ Пожалуйста, укажите email для поиска." "usage" = "❗ Пожалуйста, укажите email для поиска."
"getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>" "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>" "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>" "restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Ядро Xray успешно перезапущено." "restartSuccess" = "✅ Ядро Xray успешно перезапущено."
@ -653,8 +653,8 @@
"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." "pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль."
"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." "email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email."
"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." "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_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_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!"
"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" "cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄"
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" "error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
"using_default_value" = "Используется значение по умолчанию👌" "using_default_value" = "Используется значение по умолчанию👌"
@ -676,7 +676,7 @@
"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
"dbBackup" = "📂 Бэкап БД" "dbBackup" = "📂 Бэкап БД"
"serverUsage" = "💻 Состояние сервера" "serverUsage" = "💻 Состояние сервера"
"getInbounds" = "🔌 Подключения" "getInbounds" = "🔌 Инбаунды"
"depleteSoon" = "⚠️ Скоро конец" "depleteSoon" = "⚠️ Скоро конец"
"clientUsage" = "Статистика клиента" "clientUsage" = "Статистика клиента"
"onlines" = "🟢 Онлайн" "onlines" = "🟢 Онлайн"
@ -714,7 +714,7 @@
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Успешно!" "successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции." "errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить подключения." "getInboundsFailed" = "❌ Не удалось получить инбаунды."
"getClientsFailed" = "❌ Не удалось получить клиентов." "getClientsFailed" = "❌ Не удалось получить клиентов."
"canceled" = "❌ {{ .Email }}: Операция отменена." "canceled" = "❌ {{ .Email }}: Операция отменена."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
@ -731,5 +731,5 @@
"enableSuccess" = "✅ {{ .Email }}: Включено успешно." "enableSuccess" = "✅ {{ .Email }}: Включено успешно."
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно." "disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>" "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Выберите клиента для подключения {{ .Inbound }}" "chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}"
"chooseInbound" = "Выберите подключение" "chooseInbound" = "Выберите инбаунд"