Browse Source

Manage balancers in settings UI (#1759)

* add balancer config to ui

* manage balancer in rules table

* fix balancer translations

* fix edit button text
Saeid 1 year ago
parent
commit
c53cee31f5

+ 164 - 2
web/html/xui/xray.html

@@ -327,6 +327,14 @@
                                         [[ rule.outboundTag ]]
                                     </a-popover>
                                 </template>
+                                <template slot="balancer" slot-scope="text, rule, index">
+                                    <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                                        <template slot="content">
+                                            <p v-if="rule.balancerTag">Balancer Tag: [[ rule.balancerTag ]]</p>
+                                        </template>
+                                        [[ rule.balancerTag ]]
+                                    </a-popover>
+                                </template>
                                 <template slot="info" slot-scope="text, rule, index">
                                     <a-popover placement="bottomRight"
                                         v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
@@ -452,6 +460,41 @@
                                 </template>
                             </a-table>
                         </a-tab-pane>
+                        <a-tab-pane key="tpl-balancers" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
+                            <a-button type="primary" icon="plus" @click="addBalancer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.balancer.addBalancer"}}</a-button>
+                            <a-table :columns="balancerColumns" bordered
+                            :row-key="r => r.key"
+                            :data-source="balancersData"
+                            :scroll="isMobile ? {} : { x: 200 }"
+                            :pagination="false"
+                            :indent-size="0"
+                            :style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
+                                <template slot="action" slot-scope="text, balancer, index">
+                                    [[ index+1 ]]
+                                    <a-dropdown :trigger="['click']">
+                                        <a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
+                                        <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
+                                            <a-menu-item @click="editBalancer(index)">
+                                                <a-icon type="edit"></a-icon>
+                                                {{ i18n "edit" }}
+                                            </a-menu-item>
+                                            <a-menu-item @click="deleteBalancer(index)">
+                                                <span style="color: #FF4D4F">
+                                                    <a-icon type="delete"></a-icon> {{ i18n "delete"}}
+                                                </span>
+                                            </a-menu-item>
+                                        </a-menu>
+                                    </a-dropdown>
+                                </template>
+                                <template slot="strategy" slot-scope="text, balancer, index">
+                                    <a-tag style="margin:0;" v-if="balancer.strategy=='random'" color="purple">Random</a-tag>
+                                    <a-tag style="margin:0;" v-if="balancer.strategy=='roundRobin'" color="green">Round Robin</a-tag>
+                                </template>
+                                <template slot="selector" slot-scope="text, balancer, index">
+                                    <a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
+                                </template>
+                            </a-table>                            
+                        </a-tab-pane>
                         <a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
                             <a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
                             <a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
@@ -474,6 +517,7 @@
 {{template "ruleModal"}}
 {{template "outModal"}}
 {{template "reverseModal"}}
+{{template "balancerModal"}}
 {{template "warpModal"}}
 <script>
         const rulesColumns = [
@@ -490,9 +534,10 @@
             { title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },
             { title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]},
         { title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
-            { title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 20, ellipsis: true },
+            { title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true },
             { title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]},
-        { title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 20 },
+        { title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 15 },
+        { title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },
     ];
 
     const rulesMobileColumns = [
@@ -517,6 +562,13 @@
         { title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
     ];
 
+    const balancerColumns = [
+        { 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.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }},
+        { title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }},
+    ];
+
     const app = new Vue({
         delimiters: ['[[', ']]'],
         el: '#app',
@@ -895,6 +947,95 @@
                     this.refreshing = false;
                 }
             },
+            addBalancer() {
+                balancerModal.show({
+                    title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
+                    okText: '{{ i18n "pages.xray.balancer.addBalancer"}}',
+                    balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
+                    balancer: {
+                        tag: '',
+                        strategy: 'random',
+                        selector: []
+                    },
+                    confirm: (balancer) => {
+                        balancerModal.loading();
+                        newTemplateSettings = this.templateSettings;
+                        if (newTemplateSettings.routing.balancers == undefined) {
+                            newTemplateSettings.routing.balancers = [];
+                        }
+                        let tmpBalancer = {
+                            'tag': balancer.tag,
+                            'selector': balancer.selector
+                        };
+                        if (balancer.strategy == 'roundRobin') {
+                            tmpBalancer.strategy = {
+                                'type': balancer.strategy
+                            };
+                        }
+                        newTemplateSettings.routing.balancers.push(tmpBalancer);
+                        this.templateSettings = newTemplateSettings;
+                        balancerModal.close();
+                    },
+                    isEdit: false
+                });
+            },
+            editBalancer(index) {
+                const oldTag = this.balancersData[index].tag;
+                balancerModal.show({
+                    title: '{{ i18n "pages.xray.balancer.editBalancer"}}',
+                    okText: '{{ i18n "sure" }}',
+                    balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
+                    balancer: this.balancersData[index],
+                    confirm: (balancer) => {
+                        balancerModal.loading();
+                        newTemplateSettings = this.templateSettings;
+
+                        let tmpBalancer = {
+                            'tag': balancer.tag,
+                            'selector': balancer.selector
+                        };
+                        if (balancer.strategy == 'roundRobin') {
+                            tmpBalancer.strategy = {
+                                'type': balancer.strategy
+                            };
+                        }
+
+                        newTemplateSettings.routing.balancers[index] = tmpBalancer;
+                        // change edited tag if used in rule section
+                        if (oldTag != balancer.tag) {
+                            newTemplateSettings.routing.rules.forEach((rule) => {
+                                if (rule.balancerTag && rule.balancerTag == oldTag) {
+                                    rule.balancerTag = balancer.tag;
+                                }
+                            });
+                        }
+                        this.templateSettings = newTemplateSettings;
+                        balancerModal.close();
+                    },
+                    isEdit: true
+                });
+            },
+            deleteBalancer(index) {
+                newTemplateSettings = this.templateSettings;
+
+                //remove from balancers
+                const oldTag = this.balancersData[index].tag;
+                this.balancersData.splice(index, 1);
+
+                // remove from settings
+                let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag == oldTag);
+                newTemplateSettings.routing.balancers.splice(realIndex, 1);
+
+                // remove related routing rules
+                let rules = [];
+                newTemplateSettings.routing.rules.forEach((r) => {
+                    if (!r.balancerTag || r.balancerTag != oldTag) {
+                        rules.push(r);
+                    }
+                });
+                newTemplateSettings.routing.rules = rules;
+                this.templateSettings = newTemplateSettings;
+            },
             addReverse(){
                 reverseModal.show({
                     title: '{{ i18n "pages.xray.outbound.addReverse"}}',
@@ -1084,6 +1225,27 @@
                     return data;
                 },
             },
+            balancersData: {
+                get: function () {
+                    data = []
+                    if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {
+                        this.templateSettings.routing.balancers.forEach((o, index) => {
+                            let strategy = "random"
+                            if (o.strategy && o.strategy.type == "roundRobin") {
+                                strategy = o.strategy.type
+                            }
+
+                            data.push({ 
+                                'key': index,
+                                'tag': o.tag ? o.tag : "",
+                                'strategy': strategy,
+                                'selector': o.selector ? o.selector : []
+                            });
+                        });
+                    }
+                    return data;
+                }
+            },
             routingRuleSettings: {
                 get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
                 set: function (newValue) {

+ 111 - 0
web/html/xui/xray_balancer_modal.html

@@ -0,0 +1,111 @@
+{{define "balancerModal"}}
+<a-modal 
+    id="balancer-modal"
+    v-model="balancerModal.visible"
+    :title="balancerModal.title"
+    @ok="balancerModal.ok"
+    :confirm-loading="balancerModal.confirmLoading"
+    :ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
+    :closable="true"
+    :mask-closable="false"
+    :ok-text="balancerModal.okText"
+    cancel-text='{{ i18n "close" }}'
+    :class="themeSwitcher.currentTheme">
+    <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
+        <a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
+            :validate-status="balancerModal.duplicateTag? 'warning' : 'success'">
+            <a-input v-model.trim="balancerModal.balancer.tag" @change="balancerModal.check()"
+                placeholder='{{ i18n "balancerModal.balancer.tagDesc" }}'></a-input>
+        </a-form-item>
+        <a-form-item label='{{ i18n "pages.xray.balancer.balancerStrategy" }}'>
+            <a-select v-model="balancerModal.balancer.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
+                <a-select-option value="random">Random</a-select-option>
+                <a-select-option value="roundRobin">Round Robin</a-select-option>
+            </a-select>
+        </a-form-item>
+        <a-form-item label='{{ i18n "pages.xray.balancer.balancerSelectors" }}' has-feedback :validate-status="balancerModal.emptySelector? 'warning' : 'success'">
+            <a-select v-model="balancerModal.balancer.selector" mode="tags" @change="balancerModal.checkSelector()"
+                :dropdown-class-name="themeSwitcher.currentTheme">
+                <a-select-option v-for="tag in balancerModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
+            </a-select>
+        </a-form-item>
+        </table>
+    </a-form>
+</a-modal>
+<script>
+    const balancerModal = {
+        title: '',
+        visible: false,
+        confirmLoading: false,
+        okText: '{{ i18n "sure" }}',
+        isEdit: false,
+        confirm: null,
+        duplicateTag: false,
+        emptySelector: false,
+        balancer: {
+            tag: '',
+            strategy: 'random',
+            selector: []
+        },
+        outboundTags: [],
+        balancerTags:[],
+        ok() {
+            if (balancerModal.balancer.selector.length == 0) {
+                balancerModal.emptySelector = true;
+                return;
+            }
+            balancerModal.emptySelector = false;
+            ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
+        },
+        show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
+            this.title = title;
+            this.okText = okText;
+            this.confirm = confirm;
+            this.visible = true;
+            if (isEdit) {
+                balancerModal.balancer = balancer;
+            } else {
+                balancerModal.balancer = {
+                    tag: '',
+                    strategy: 'random',
+                    selector: []
+                };
+            }
+            this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
+            this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
+            this.isEdit = isEdit;
+            this.check()
+        },
+        close() {
+            balancerModal.visible = false;
+            balancerModal.loading(false);
+        },
+        loading(loading) {
+            balancerModal.confirmLoading = loading;
+        },
+        check() {
+            if (balancerModal.balancer.tag == '' || balancerModal.balancerTags.includes(balancerModal.balancer.tag)) {
+                this.duplicateTag = true;
+                this.isValid = false;
+            } else {
+                this.duplicateTag = false;
+                this.isValid = true;
+            }
+        },
+        checkSelector() {
+            balancerModal.emptySelector = balancerModal.balancer.selector.length == 0;
+        }
+    };
+
+    new Vue({
+        delimiters: ['[[', ']]'],
+        el: '#balancer-modal',
+        data: {
+            balancerModal: balancerModal
+        },
+        methods: {
+        }
+    });
+
+</script>
+{{end}}

+ 22 - 1
web/html/xui/xray_rule_modal.html

@@ -107,6 +107,19 @@
                 <a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
             </a-select>
         </a-form-item>
+        <a-form-item>
+            <template slot="label">
+                <a-tooltip>
+                    <template slot="title">
+                        <span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
+                    </template>
+                    Balancer Tag <a-icon type="question-circle"></a-icon>
+                </a-tooltip>
+            </template>
+            <a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
+                <a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
+            </a-select>
+        </a-form-item>
     </table>
     </a-form>
 </a-modal>
@@ -133,11 +146,12 @@
             protocol: [],
             attrs: [],
             outboundTag: "",
+            balancerTag: "",
         },
         inboundTags: [],
         outboundTags: [],
         users: [],
-        balancerTag: [],
+        balancerTags: [],
         ok() {
             newRule = ruleModal.getResult();
             ObjectUtil.execute(ruleModal.confirm, newRule);
@@ -160,6 +174,7 @@
                 this.rule.protocol = rule.protocol;
                 this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
                 this.rule.outboundTag = rule.outboundTag;
+                this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : ""
             } else {
                 this.rule = {
                     domainMatcher: "",
@@ -174,6 +189,7 @@
                     protocol: [],
                     attrs: [],
                     outboundTag: "",
+                    balancerTag: "",
                 }
             }
             this.isEdit = isEdit;
@@ -186,6 +202,10 @@
                 }
                 if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
             }
+
+            if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
+                this.balancerTags = app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)
+            }
         },
         close() {
             ruleModal.visible = false;
@@ -211,6 +231,7 @@
             rule.protocol = value.protocol;
             rule.attrs = Object.fromEntries(value.attrs);
             rule.outboundTag = value.outboundTag;
+            rule.balancerTag = value.balancerTag;
 
             for (const [key, value] of Object.entries(rule)) {
                 if (

+ 11 - 0
web/translation/translate.en_US.toml

@@ -388,6 +388,7 @@
 "Inbounds" = "Inbounds"
 "InboundsDesc" = "Accepting the specific clients."
 "Outbounds" = "Outbounds"
+"Balancers" = "Balancers"
 "OutboundsDesc" = "Set the outgoing traffic pathway."
 "Routings" = "Routing Rules"
 "RoutingsDesc" = "The priority of each rule is important!"
@@ -406,6 +407,7 @@
 "dest" = "Destination"
 "inbound" = "Inbound"
 "outbound" = "Outbound"
+"balancer" = "Balancer"
 "info" = "Info"
 "add" = "Add Rule"
 "edit" = "Edit Rule"
@@ -426,6 +428,15 @@
 "portal" = "Portal"
 "intercon" = "Interconnection"
 
+[pages.xray.balancer]
+"addBalancer" = "Add Balancer"
+"editBalancer" = "Edit Balancer"
+"balancerStrategy" = "Strategy"
+"balancerSelectors" = "Selectors"
+"tag" = "Tag"
+"tagDesc" = "Unique Tag"
+"balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work."
+
 [pages.xray.wireguard]
 "secretKey" = "Secret Key"
 "publicKey" = "Public Key"

+ 11 - 0
web/translation/translate.es_ES.toml

@@ -388,6 +388,7 @@
 "Inbounds" = "Entrante"
 "InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
 "Outbounds" = "Salidas"
+"Balancers" = "Equilibradores"
 "OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
 "Routings" = "Reglas de enrutamiento"
 "RoutingsDesc" = "¡La prioridad de cada regla es importante!"
@@ -406,6 +407,7 @@
 "dest" = "Destino"
 "inbound" = "Entrante"
 "outbound" = "saliente"
+"balancer" = "Balancín"
 "info" = "Información"
 "add" = "Agregar regla"
 "edit" = "Editar regla"
@@ -426,6 +428,15 @@
 "portal" = "portal"
 "intercon" = "Interconexión"
 
+[pages.xray.balancer]
+"addBalancer" = "Agregar equilibrador"
+"editBalancer" = "Editar balanceador"
+"balancerStrategy" = "Estrategia"
+"balancerSelectors" = "Selectores"
+"tag" = "Etiqueta"
+"tagDesc" = "etiqueta única"
+"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag."
+
 [pages.xray.wireguard]
 "secretKey" = "Llave secreta"
 "publicKey" = "Llave pública"

+ 11 - 0
web/translation/translate.fa_IR.toml

@@ -388,6 +388,7 @@
 "Inbounds" = "ورودی‌ها"
 "InboundsDesc" = "پذیرش کلاینت خاص"
 "Outbounds" = "خروجی‌ها"
+"Balancers" = "بالانسرها"
 "OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
 "Routings" = "قوانین مسیریابی"
 "RoutingsDesc" = "اولویت هر قانون مهم است"
@@ -406,6 +407,7 @@
 "dest" = "مقصد"
 "inbound" = "ورودی"
 "outbound" = "خروجی"
+"balancer" = "بالانسر"
 "info" = "اطلاعات"
 "add" = "افزودن قانون"
 "edit" = "ویرایش قانون"
@@ -426,6 +428,15 @@
 "portal" = "پورتال"
 "intercon" = "اتصال میانی"
 
+[pages.xray.balancer]
+"addBalancer" = "افزودن بالانسر"
+"editBalancer" = "ویرایش بالانسر"
+"balancerStrategy" = "استراتژی"
+"balancerSelectors" = "انتخاب‌گرها"
+"tag" = "برچسب"
+"tagDesc" = "برچسب یگانه"
+"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
+
 [pages.xray.wireguard]
 "secretKey" = "کلید شخصی"
 "publicKey" = "کلید عمومی"

+ 11 - 0
web/translation/translate.ru_RU.toml

@@ -388,6 +388,7 @@
 "Inbounds" = "Входящие"
 "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей"
 "Outbounds" = "Исходящие"
+"Balancers" = "Балансиры"
 "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
 "Routings" = "Правила маршрутизации"
 "RoutingsDesc" = "Важен приоритет каждого правила!"
@@ -406,6 +407,7 @@
 "dest" = "Пункт назначения"
 "inbound" = "Входящий"
 "outbound" = "Исходящий"
+"balancer" = "балансир"
 "info" = "Информация"
 "add" = "Добавить правило"
 "edit" = "Редактировать правило"
@@ -426,6 +428,15 @@
 "portal" = "Портал"
 "intercon" = "Соединение"
 
+[pages.xray.balancer]
+"addBalancer" = "Добавить балансир"
+"editBalancer" = "Редактировать балансир"
+"balancerStrategy" = "Стратегия"
+"balancerSelectors" = "Селекторы"
+"tag" = "Тег"
+"tagDesc" = "уникальный тег"
+"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
+
 [pages.xray.wireguard]
 "secretKey" = "Секретный ключ"
 "publicKey" = "Открытый ключ"

+ 11 - 0
web/translation/translate.vi_VN.toml

@@ -388,6 +388,7 @@
 "Inbounds" = "Đầu vào"
 "InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
 "Outbounds" = "Đầu ra"
+"Balancers" = "Cân bằng"
 "OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
 "Routings" = "Quy tắc định tuyến"
 "RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
@@ -406,6 +407,7 @@
 "dest" = "Đích"
 "inbound" = "Vào"
 "outbound" = "Ra"
+"balancer" = "Cân bằng"
 "info" = "Thông tin"
 "add" = "Thêm quy tắc"
 "edit" = "Chỉnh sửa quy tắc"
@@ -426,6 +428,15 @@
 "portal" = "Cổng thông tin"
 "intercon" = "Kết nối"
 
+[pages.xray.balancer]
+"addBalancer" = "Thêm cân bằng"
+"editBalancer" = "Chỉnh sửa cân bằng"
+"balancerStrategy" = "Chiến lược"
+"balancerSelectors" = "Bộ chọn"
+"tag" = "Thẻ"
+"tagDesc" = "thẻ duy nhất"
+"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động."
+
 [pages.xray.wireguard]
 "secretKey" = "Khoá bí mật"
 "publicKey" = "Khóa công khai"

+ 11 - 0
web/translation/translate.zh_Hans.toml

@@ -388,6 +388,7 @@
 "Inbounds" = "入站"
 "InboundsDesc" = "更改配置模板接受特殊客户端"
 "Outbounds" = "出站"
+"Balancers" = "平衡器"
 "OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
 "Routings" = "路由规则"
 "RoutingsDesc" = "每条规则的优先级都很重要"
@@ -406,6 +407,7 @@
 "dest" = "目的地"
 "inbound" = "入站"
 "outbound" = "出站"
+"balancer" = "平衡器"
 "info" = "信息"
 "add" = "添加规则"
 "edit" = "编辑规则"
@@ -426,6 +428,15 @@
 "portal" = "门户"
 "intercon" = "互连"
 
+[pages.xray.balancer]
+"addBalancer" = "添加平衡器"
+"editBalancer" = "编辑平衡器"
+"balancerStrategy" = "战略"
+"balancerSelectors" = "选择器"
+"tag" = "标签"
+"tagDesc" = "唯一标记"
+"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用,则只有outboundTag起作用。"
+
 [pages.xray.wireguard]
 "secretKey" = "密钥"
 "publicKey" = "公钥"