diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index 122145fb..dd602c46 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -141,6 +141,19 @@ class DBInbound { return Inbound.fromJson(config); } + isMultiUser() { + switch (this.protocol) { + case Protocols.VMESS: + case Protocols.VLESS: + case Protocols.TROJAN: + return true; + case Protocols.SHADOWSOCKS: + return this.toInbound().isSSMultiUser; + default: + return false; + } + } + hasLink() { switch (this.protocol) { case Protocols.VMESS: diff --git a/web/assets/js/util/common.js b/web/assets/js/util/common.js index 8e30bce7..b2f15fb2 100644 --- a/web/assets/js/util/common.js +++ b/web/assets/js/util/common.js @@ -52,13 +52,15 @@ function safeBase64(str) { function formatSecond(second) { if (second < 60) { - return second.toFixed(0) + ' s'; + return second.toFixed(0) + 's'; } else if (second < 3600) { - return (second / 60).toFixed(0) + ' m'; + return (second / 60).toFixed(0) + 'm'; } else if (second < 3600 * 24) { - return (second / 3600).toFixed(0) + ' h'; + return (second / 3600).toFixed(0) + 'h'; } else { - return (second / 3600 / 24).toFixed(0) + ' d'; + day = Math.floor(second / 3600 / 24); + remain = ((second/3600) - (day*24)).toFixed(0); + return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : ''); } } @@ -72,7 +74,7 @@ function addZero(num) { function toFixed(num, n) { n = Math.pow(10, n); - return Math.round(num * n) / n; + return Math.floor(num * n) / n; } function debounce(fn, delay) { @@ -115,15 +117,39 @@ function setCookie(cname, cvalue, exdays) { function usageColor(data, threshold, total) { switch (true) { case data === null: - return 'blue'; - case total <= 0: - return 'blue'; + return "green"; + case total < 0: + return "blue"; + case total == 0: + return "purple"; case data < total - threshold: - return 'cyan'; + return "blue"; case data < total: - return 'orange'; + return "orange"; default: - return 'red'; + return "red"; + } +} + +function userExpiryColor(threshold, client, isDark = false) { + if (!client.enable) { + return isDark ? '#2c3950' : '#bcbcbc'; + } + now = new Date().getTime(), + expiry = client.expiryTime; + switch (true) { + case expiry === null: + return "#389e0d"; + case expiry < 0: + return "#0e49b5"; + case expiry == 0: + return "#7a316f"; + case now < expiry - threshold: + return "#0e49b5"; + case now < expiry: + return "#ffa031"; + default: + return "#e04141"; } } diff --git a/web/html/common/prompt_modal.html b/web/html/common/prompt_modal.html index 17a65ec1..a61b4454 100644 --- a/web/html/common/prompt_modal.html +++ b/web/html/common/prompt_modal.html @@ -2,7 +2,7 @@ <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" :closable="true" @ok="promptModal.ok" :mask-closable="false" :class="themeSwitcher.darkCardClass" - :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> + :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme"> <a-input id="prompt-modal-input" :type="promptModal.type" v-model="promptModal.value" :autosize="{minRows: 10, maxRows: 20}" diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index 51dc38cb..2db1948b 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -1,7 +1,7 @@ {{define "qrcodeModal"}} <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" :closable="true" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" :footer="null" width="300px"> <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;"> @@ -13,7 +13,7 @@ </template> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider> <template v-for="(row, index) in qrModal.qrcodes"> - <a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag> + <a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag> <canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas> </template> </a-modal> diff --git a/web/html/common/text_modal.html b/web/html/common/text_modal.html index 1514051b..2b455ae4 100644 --- a/web/html/common/text_modal.html +++ b/web/html/common/text_modal.html @@ -1,7 +1,7 @@ {{define "textModal"}} <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" diff --git a/web/html/xui/client_bulk_modal.html b/web/html/xui/client_bulk_modal.html index d5c60b76..f758f627 100644 --- a/web/html/xui/client_bulk_modal.html +++ b/web/html/xui/client_bulk_modal.html @@ -1,11 +1,11 @@ {{define "clientsBulkModal"}} <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'> <a-form layout="inline"> <a-form-item label='{{ i18n "pages.client.method" }}'> - <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option :value="0">Random</a-select-option> <a-select-option :value="1">Random+Prefix</a-select-option> <a-select-option :value="2">Random+Prefix+Num</a-select-option> @@ -72,13 +72,13 @@ </a-form-item> <br> <a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow"> - <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-select> </a-form-item> <a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline"> - <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-select> diff --git a/web/html/xui/client_modal.html b/web/html/xui/client_modal.html index e7c23098..b2c03129 100644 --- a/web/html/xui/client_modal.html +++ b/web/html/xui/client_modal.html @@ -1,8 +1,11 @@ {{define "clientsModal"}} <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok" :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'> + <template v-if="isEdit"> + <a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag> + </template> {{template "form/client"}} </a-modal> <script> @@ -151,7 +154,7 @@ this.$confirm({ title: '{{ i18n "pages.inbounds.resetTraffic"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: async () => { diff --git a/web/html/xui/common_sider.html b/web/html/xui/common_sider.html index 58072f82..12a8ed41 100644 --- a/web/html/xui/common_sider.html +++ b/web/html/xui/common_sider.html @@ -11,6 +11,10 @@ <a-icon type="setting"></a-icon> <span>{{ i18n "menu.settings"}}</span> </a-menu-item> +<a-menu-item key="{{ .base_path }}xui/xray"> + <a-icon type="tool"></a-icon> + <span>{{ i18n "menu.xray"}}</span> +</a-menu-item> <!--<a-menu-item key="{{ .base_path }}panel/clients">--> <!-- <a-icon type="laptop"></a-icon>--> <!-- <span>Client</span>--> @@ -26,7 +30,7 @@ <a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0"> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys=""> <a-menu-item mode="inline"> - <a-icon type="bg-colors"></a-icon> + <a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon> <theme-switch /> </a-menu-item> </a-menu> @@ -38,14 +42,14 @@ <a-drawer id="sider-drawer" placement="left" :closable="false" @close="siderDrawer.close()" :visible="siderDrawer.visible" - :wrap-class-name="themeSwitcher.darkDrawerClass" + :wrap-class-name="themeSwitcher.currentTheme" :wrap-style="{ padding: 0 }"> <div class="drawer-handle" @click="siderDrawer.change()" slot="handle"> <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon> </div> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys=""> <a-menu-item mode="inline"> - <a-icon type="bg-colors"></a-icon> + <a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon> <theme-switch /> </a-menu-item> </a-menu> diff --git a/web/html/xui/component/password.html b/web/html/xui/component/password.html index ebea3be6..13dbfb51 100644 --- a/web/html/xui/component/password.html +++ b/web/html/xui/component/password.html @@ -4,12 +4,12 @@ :placeholder="placeholder" @input="$emit('input', $event.target.value)"> <template v-if="icon" #prefix> - <a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" /> + <a-icon :type="icon" style="font-size: 16px;" /> </template> <template #addonAfter> <a-icon :type="showPassword ? 'eye-invisible' : 'eye'" @click="toggleShowPassword" - :style="'font-size: 16px;' + themeSwitcher.textStyle" /> + style="font-size: 16px;" /> </template> </a-input> </template> diff --git a/web/html/xui/component/themeSwitch.html b/web/html/xui/component/themeSwitch.html index 7cbedecf..4d5412e2 100644 --- a/web/html/xui/component/themeSwitch.html +++ b/web/html/xui/component/themeSwitch.html @@ -1,8 +1,6 @@ {{define "component/themeSwitchTemplate"}} <template> - <a-switch :default-checked="themeSwitcher.isDarkTheme" - checked-children="☀" - un-checked-children="🌙" + <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"> </a-switch> </template> @@ -10,39 +8,17 @@ {{define "component/themeSwitcher"}} <script> - const colors = { - dark: { - bg: "#242c3a", - text: "hsla(0,0%,100%,.65)" - }, - light: { - bg: '#f0f2f5', - text: "rgba(0, 0, 0, 0.7)", - } - } - function createThemeSwitcher() { const isDarkTheme = localStorage.getItem('dark-mode') === 'true'; const theme = isDarkTheme ? 'dark' : 'light'; return { isDarkTheme, - bgStyle: `background: ${colors[theme].bg};`, - textStyle: `color: ${colors[theme].text};`, - darkClass: isDarkTheme ? 'ant-dark' : '', - darkCardClass: isDarkTheme ? 'ant-card-dark' : '', - darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '', get currentTheme() { return this.isDarkTheme ? 'dark' : 'light'; }, toggleTheme() { this.isDarkTheme = !this.isDarkTheme; - this.theme = this.isDarkTheme ? 'dark' : 'light'; localStorage.setItem('dark-mode', this.isDarkTheme); - this.bgStyle = `background: ${colors[this.theme].bg};`; - this.textStyle = `color: ${colors[this.theme].text};`; - this.darkClass = this.isDarkTheme ? 'ant-dark' : ''; - this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : ''; - this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : ''; }, }; } diff --git a/web/html/xui/form/client.html b/web/html/xui/form/client.html index 4fd5b8e3..63080ab7 100644 --- a/web/html/xui/form/client.html +++ b/web/html/xui/form/client.html @@ -1,10 +1,5 @@ {{define "form/client"}} <a-form layout="inline" v-if="client"> - <template v-if="isEdit"> - <a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;"> - Account is (Expired|Traffic Ended) And Disabled - </a-tag> - </template> <a-form-item label='{{ i18n "pages.inbounds.enable" }}'> <a-switch v-model="client.enable"></a-switch> </a-form-item> @@ -97,13 +92,13 @@ </a-form-item> <br> <a-form-item v-if="inbound.xtls" label="Flow"> - <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-select> </a-form-item> <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow"> - <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> </a-select> @@ -136,10 +131,10 @@ </a-form-item> <br> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'> - <a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch> + <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> </a-form-item> <br> - <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart"> + <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="delayedStart"> <a-input-number v-model="delayedExpireDays" :min="0"></a-input-number> </a-form-item> <a-form-item v-else> @@ -153,9 +148,21 @@ </a-tooltip> </span> <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="themeSwitcher.darkCardClass" + :dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime" style="width: 170px;"></a-date-picker> - <a-tag color="red" v-if="isExpiry">Expired</a-tag> + <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag> + </a-form-item> + <a-form-item v-if="client.expiryTime != 0"> + <span slot="label"> + <span>{{ i18n "pages.client.renew" }}</span> + <a-tooltip> + <template slot="title"> + <span>{{ i18n "pages.client.renewDesc" }}</span> + </template> + <a-icon type="question-circle" theme="filled"></a-icon> + </a-tooltip> + </span> + <a-input-number v-model.number="client.reset" :min="0"></a-input-number> </a-form-item> </a-form> {{end}} \ No newline at end of file diff --git a/web/html/xui/form/inbound.html b/web/html/xui/form/inbound.html index e5b3bfc3..40a96d1e 100644 --- a/web/html/xui/form/inbound.html +++ b/web/html/xui/form/inbound.html @@ -9,7 +9,7 @@ <a-input v-model.trim="dbInbound.remark"></a-input> </a-form-item> <a-form-item label='{{ i18n "protocol" }}'> - <a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> </a-select> </a-form-item> @@ -53,7 +53,7 @@ </a-tooltip> </span> <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="themeSwitcher.darkCardClass" + :dropdown-class-name="themeSwitcher.currentTheme" v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker> </a-form-item> </a-form> diff --git a/web/html/xui/form/protocol/dokodemo.html b/web/html/xui/form/protocol/dokodemo.html index 0eed31d4..6a37f346 100644 --- a/web/html/xui/form/protocol/dokodemo.html +++ b/web/html/xui/form/protocol/dokodemo.html @@ -8,7 +8,7 @@ </a-form-item> <br> <a-form-item label='{{ i18n "pages.inbounds.network"}}'> - <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="tcp,udp">TCP+UDP</a-select-option> <a-select-option value="tcp">TCP</a-select-option> <a-select-option value="udp">UDP</a-select-option> diff --git a/web/html/xui/form/protocol/shadowsocks.html b/web/html/xui/form/protocol/shadowsocks.html index 486b372f..001a7006 100644 --- a/web/html/xui/form/protocol/shadowsocks.html +++ b/web/html/xui/form/protocol/shadowsocks.html @@ -3,94 +3,7 @@ <template v-if="inbound.isSSMultiUser"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.email" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.emailDesc" }}</span> - </template> - </a-tooltip> - </span> - <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> - <a-input v-model.trim="client.email" style="width: 200px;"></a-input> - </a-form-item> - <a-form-item label="Password"> - <a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon> - <a-input v-model.trim="client.password" style="width: 250px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.subSettings.enable"> - <span slot="label"> - Subscription - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> - <a-input v-model.trim="client.subId" style="width: 150px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.tgBotEnable"> - <span slot="label"> - Telegram ID - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.telegramDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input v-model.trim="client.tgId"></a-input> - </a-form-item> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.IPLimit" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client.limitIp" min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) - <a-tooltip> - <template slot="title"> - 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client._totalGB" :min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item label='{{ i18n "pages.client.delayedStart" }}'> - <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> - </a-form-item> - <br> - <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> - <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> - </a-form-item> - <a-form-item v-else> - <span slot="label"> - <span>{{ i18n "pages.inbounds.expireDate" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="themeSwitcher.darkCardClass" - v-model="client._expiryTime" style="width: 170px;"></a-date-picker> - </a-form-item> + {{template "form/client"}} </a-collapse-panel> </a-collapse> <a-collapse v-else> @@ -111,7 +24,7 @@ </a-form> <a-form layout="inline"> <a-form-item label='{{ i18n "encryption" }}'> - <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange"> + <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme" @change="SSMethodChange"> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> </a-select> </a-form-item> @@ -120,7 +33,7 @@ <a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input> </a-form-item> <a-form-item label='{{ i18n "pages.inbounds.network" }}'> - <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="tcp,udp">TCP+UDP</a-select-option> <a-select-option value="tcp">TCP</a-select-option> <a-select-option value="udp">UDP</a-select-option> diff --git a/web/html/xui/form/protocol/trojan.html b/web/html/xui/form/protocol/trojan.html index 46ae5e32..b2ac3dba 100644 --- a/web/html/xui/form/protocol/trojan.html +++ b/web/html/xui/form/protocol/trojan.html @@ -2,100 +2,7 @@ <a-form layout="inline" style="padding: 10px 0px;"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.email" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.emailDesc" }}</span> - </template> - </a-tooltip> - </span> - <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> - <a-input v-model.trim="client.email" style="width: 200px;"></a-input> - </a-form-item> - <a-form-item label="Password"> - <a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon> - <a-input v-model.trim="client.password" style="width: 150px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.subSettings.enable"> - <span slot="label"> - Subscription - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> - <a-input v-model.trim="client.subId" style="width: 150px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.tgBotEnable"> - <span slot="label"> - Telegram ID - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.telegramDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input v-model.trim="client.tgId"></a-input> - </a-form-item> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.IPLimit" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client.limitIp" min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item v-if="inbound.xtls" label="Flow"> - <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass"> - <a-select-option value="">{{ i18n "none" }}</a-select-option> - <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> - </a-select> - </a-form-item> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) - <a-tooltip> - <template slot="title"> - 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client._totalGB" :min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item label='{{ i18n "pages.client.delayedStart" }}'> - <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> - </a-form-item> - <br> - <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> - <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> - </a-form-item> - <a-form-item v-else> - <span slot="label"> - <span>{{ i18n "pages.inbounds.expireDate" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="themeSwitcher.darkCardClass" - v-model="client._expiryTime" style="width: 170px;"></a-date-picker> - </a-form-item> + {{template "form/client"}} </a-collapse-panel> </a-collapse> <a-collapse v-else> @@ -126,7 +33,7 @@ <!-- trojan fallbacks --> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> - <a-divider> + <a-divider style="margin:0;"> fallback[[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"/> @@ -146,7 +53,7 @@ <a-form-item label="xVer"> <a-input-number v-model="fallback.xver"></a-input-number> </a-form-item> - <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/> </a-form> + <a-divider style="margin:0;"></a-divider> </template> {{end}} \ No newline at end of file diff --git a/web/html/xui/form/protocol/vless.html b/web/html/xui/form/protocol/vless.html index 12729eff..261e6616 100644 --- a/web/html/xui/form/protocol/vless.html +++ b/web/html/xui/form/protocol/vless.html @@ -2,106 +2,7 @@ <a-form layout="inline" style="padding: 10px 0px;"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.email" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.emailDesc" }}</span> - </template> - </a-tooltip> - </span> - <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> - <a-input v-model.trim="client.email" style="width: 200px;"></a-input> - </a-form-item> - <a-form-item label="ID"> - <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon> - <a-input v-model.trim="client.id" style="width: 300px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.subSettings.enable"> - <span slot="label"> - Subscription - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> - <a-input v-model.trim="client.subId" style="width: 150px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.tgBotEnable"> - <span slot="label"> - Telegram ID - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.telegramDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input v-model.trim="client.tgId"></a-input> - </a-form-item> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.IPLimit" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client.limitIp" min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item v-if="inbound.xtls" label="Flow"> - <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> - <a-select-option value="" selected>{{ i18n "none" }}</a-select-option> - <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> - </a-select> - </a-form-item> - <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow"> - <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass"> - <a-select-option value="" selected>{{ i18n "none" }}</a-select-option> - <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> - </a-select> - </a-form-item> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) - <a-tooltip> - <template slot="title"> - 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client._totalGB" :min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item label='{{ i18n "pages.client.delayedStart" }}'> - <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> - </a-form-item> - <br> - <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> - <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> - </a-form-item> - <a-form-item v-else> - <span slot="label"> - <span>{{ i18n "pages.inbounds.expireDate" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="themeSwitcher.darkCardClass" - v-model="client._expiryTime" style="width: 170px;"></a-date-picker> - </a-form-item> + {{template "form/client"}} </a-collapse-panel> </a-collapse> <a-collapse v-else> @@ -134,7 +35,7 @@ <!-- vless fallbacks --> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> - <a-divider> + <a-divider style="margin:0;"> fallback[[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"/> @@ -154,7 +55,6 @@ <a-form-item label="xVer"> <a-input-number v-model="fallback.xver"></a-input-number> </a-form-item> - <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/> </a-form> </template> {{end}} diff --git a/web/html/xui/form/protocol/vmess.html b/web/html/xui/form/protocol/vmess.html index c008f1c0..6d89c1b5 100644 --- a/web/html/xui/form/protocol/vmess.html +++ b/web/html/xui/form/protocol/vmess.html @@ -2,95 +2,7 @@ <a-form layout="inline" style="padding: 10px 0px;"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.email" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.emailDesc" }}</span> - </template> - </a-tooltip> - </span> - <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon> - <a-input v-model.trim="client.email" style="width: 200px;"></a-input> - </a-form-item> - <br> - <a-form-item label="ID"> - <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon> - <a-input v-model.trim="client.id" style="width: 300px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.subSettings.enable"> - <span slot="label"> - Subscription - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon> - <a-input v-model.trim="client.subId" style="width: 150px;"></a-input> - </a-form-item> - <a-form-item v-if="client.email && app.tgBotEnable"> - <span slot="label"> - Telegram ID - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.telegramDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input v-model.trim="client.tgId"></a-input> - </a-form-item> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.IPLimit" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client.limitIp" min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item> - <span slot="label"> - <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) - <a-tooltip> - <template slot="title"> - 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-input-number v-model="client._totalGB" :min="0"></a-input-number> - </a-form-item> - <br> - <a-form-item label='{{ i18n "pages.client.delayedStart" }}'> - <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> - </a-form-item> - <br> - <a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> - <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> - </a-form-item> - <a-form-item v-else> - <span slot="label"> - <span>{{ i18n "pages.inbounds.expireDate" }}</span> - <a-tooltip> - <template slot="title"> - <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> - </template> - <a-icon type="question-circle" theme="filled"></a-icon> - </a-tooltip> - </span> - <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" - :dropdown-class-name="themeSwitcher.darkCardClass" - v-model="client._expiryTime" style="width: 170px;"></a-date-picker> - </a-form-item> + {{template "form/client"}} </a-collapse-panel> </a-collapse> <a-collapse v-else> diff --git a/web/html/xui/form/stream/stream_kcp.html b/web/html/xui/form/stream/stream_kcp.html index 0d6df9e3..59b708bf 100644 --- a/web/html/xui/form/stream/stream_kcp.html +++ b/web/html/xui/form/stream/stream_kcp.html @@ -1,7 +1,7 @@ {{define "form/streamKCP"}} <a-form layout="inline"> <a-form-item label='{{ i18n "camouflage" }}'> - <a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="none">None (Not Camouflage)</a-select-option> <a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option> <a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option> diff --git a/web/html/xui/form/stream/stream_quic.html b/web/html/xui/form/stream/stream_quic.html index 53e60b2d..d4e22dd7 100644 --- a/web/html/xui/form/stream/stream_quic.html +++ b/web/html/xui/form/stream/stream_quic.html @@ -1,7 +1,7 @@ {{define "form/streamQUIC"}} <a-form layout="inline"> <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'> - <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="none">none</a-select-option> <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option> <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option> @@ -12,7 +12,7 @@ <a-input v-model.trim="inbound.stream.quic.key" style="width: 150px;"></a-input> </a-form-item> <a-form-item label='{{ i18n "camouflage" }}'> - <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="none">none (not camouflage)</a-select-option> <a-select-option value="srtp">srtp (camouflage video call)</a-select-option> <a-select-option value="utp">utp (camouflage BT download)</a-select-option> diff --git a/web/html/xui/form/stream/stream_settings.html b/web/html/xui/form/stream/stream_settings.html index e50caf19..cac65207 100644 --- a/web/html/xui/form/stream/stream_settings.html +++ b/web/html/xui/form/stream/stream_settings.html @@ -2,7 +2,7 @@ <!-- select stream network --> <a-form layout="inline"> <a-form-item label='{{ i18n "transmission" }}'> - <a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="tcp">TCP</a-select-option> <a-select-option value="kcp">KCP</a-select-option> <a-select-option value="ws">WS</a-select-option> diff --git a/web/html/xui/form/stream/stream_sockopt.html b/web/html/xui/form/stream/stream_sockopt.html index d90bf206..5da2f8af 100644 --- a/web/html/xui/form/stream/stream_sockopt.html +++ b/web/html/xui/form/stream/stream_sockopt.html @@ -33,7 +33,7 @@ <td> <a-form-item> <a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;" - :dropdown-class-name="themeSwitcher.darkCardClass"> + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="off">OFF</a-select-option> <a-select-option value="redirect">Redirect</a-select-option> <a-select-option value="tproxy">T-Proxy</a-select-option> diff --git a/web/html/xui/form/tls_settings.html b/web/html/xui/form/tls_settings.html index ce3e0fe4..493aef75 100644 --- a/web/html/xui/form/tls_settings.html +++ b/web/html/xui/form/tls_settings.html @@ -1,6 +1,7 @@ {{define "form/tlsSettings"}} <!-- tls enable --> <a-form layout="inline" v-if="inbound.canSetTls()"> + <a-divider style="margin:0;"></a-divider> <a-form-item v-if="inbound.canEnableTls()" label="TLS"> <a-switch v-model="inbound.tls"> </a-switch> @@ -54,27 +55,27 @@ <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input> </a-form-item> <a-form-item label="CipherSuites"> - <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass"> + <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="">auto</a-select-option> - <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> + <a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option> </a-select> </a-form-item> - <a-form-item label="MinVersion"> - <a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass"> - <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> - </a-select> - </a-form-item> - <a-form-item label="MaxVersion"> - <a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass"> - <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> - </a-select> + <a-form-item label="Min/Max Version"> + <a-input-group compact> + <a-select style="width: 60px" v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> + </a-select> + <a-select style="width: 60px" v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> + </a-select> + </a-input-group> </a-form-item> <a-form-item label="SNI" placeholder="Server Name Indication"> <a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input> </a-form-item> <a-form-item label="uTLS"> <a-select v-model="inbound.stream.tls.settings.fingerprint" - style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass"> + style="width: 170px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value=''>None</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> </a-select> @@ -83,7 +84,7 @@ <a-select mode="multiple" style="width: 250px" - :dropdown-class-name="themeSwitcher.darkCardClass" + :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn"> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> </a-select> @@ -185,7 +186,7 @@ </a-form-item> <a-form-item label="uTLS"> <a-select v-model="inbound.stream.reality.settings.fingerprint" - style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass"> + style="width: 135px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> </a-select> </a-form-item> diff --git a/web/html/xui/inbound_client_table.html b/web/html/xui/inbound_client_table.html index 138c91c3..5109fb28 100644 --- a/web/html/xui/inbound_client_table.html +++ b/web/html/xui/inbound_client_table.html @@ -2,34 +2,66 @@ <template slot="actions" slot-scope="text, client, index"> <a-tooltip> <template slot="title">{{ i18n "qrCode" }}</template> - <a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon> + <a-icon style="font-size: 24px;" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon> </a-tooltip> <a-tooltip> <template slot="title">{{ i18n "pages.client.edit" }}</template> - <a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon> + <a-icon style="font-size: 24px;" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon> </a-tooltip> <a-tooltip> <template slot="title">{{ i18n "info" }}</template> - <a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon> + <a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record,index);"></a-icon> </a-tooltip> <a-tooltip> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> - <a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon> + <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" + title='{{ i18n "pages.inbounds.resetTrafficContent"}}' + :overlay-class-name="themeSwitcher.currentTheme" + ok-text='{{ i18n "reset"}}' + cancel-text='{{ i18n "cancel"}}'> + <a-icon slot="icon" type="question-circle-o" style="color: blue"></a-icon> + <a-icon style="font-size: 24px;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon> + </a-popconfirm> </a-tooltip> <a-tooltip> <template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template> - <a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon> + <a-popconfirm @confirm="delClient(record.id,client,false)" + title='{{ i18n "pages.inbounds.deleteClientContent"}}' + :overlay-class-name="themeSwitcher.currentTheme" + ok-text='{{ i18n "delete"}}' + ok-type="danger" + cancel-text='{{ i18n "cancel"}}'> + <a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon> + <a-icon style="font-size: 24px" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon> + </a-popconfirm> </a-tooltip> </template> <template slot="enable" slot-scope="text, client, index"> <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch> </template> +<template slot="online" slot-scope="text, client, index"> + <template v-if="isClientOnline(client.email)"> + <a-tag color="green">{{ i18n "online" }}</a-tag> + </template> + <template v-else> + <a-tag>{{ i18n "offline" }}</a-tag> + </template> +</template> <template slot="client" slot-scope="text, client"> + <a-tooltip> + <template slot="title"> + <template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template> + <template v-else-if="!client.enable">{{ i18n "disabled" }}</template> + <template v-else-if="isClientOnline(client.email)">{{ i18n "online" }}</template> + </template> + <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"> + </a-badge> + </a-tooltip> [[ client.email ]] <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag> </template> <template slot="traffic" slot-scope="text, client"> - <a-popover :overlay-class-name="themeSwitcher.darkClass"> + <a-popover :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content" v-if="client.email"> <table cellpadding="2" width="100%"> <tr> @@ -38,28 +70,200 @@ </tr> <tr v-if="client.totalGB > 0"> <td>{{ i18n "remained" }}</td> - <td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td> + <td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td> </tr> </table> </template> - <a-tag :color="statsColor(record, client.email)"> - [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / - <template v-if="client.totalGB > 0">[[client._totalGB]]GB</template> - <template v-else> - <svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg> - </template> - </a-tag> + <table> + <tr> + <td width="80px" style="margin:0; text-align: right;font-size: 1em;"> + [[ sizeFormat(getSumStats(record, client.email)) ]] + </td> + <td width="120px" v-if="!client.enable"> + <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" + :show-info="false" + :percent="statsProgress(record, client.email)"/> + </td> + <td width="120px" v-else-if="client.totalGB > 0"> + <a-progress :stroke-color="statsColor(record, client.email)" + :show-info="false" + :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''" + :percent="statsProgress(record, client.email)"/> + </td> + <td width="120px" v-else class="infinite-bar"> + <a-progress + :show-info="false" + :status="isClientOnline(client.email)? 'active' : ''" + :percent="100"></a-progress> + </td> + <td width="60px"> + <template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template> + <span v-else style="font-weight: 100;font-size: 14pt;">∞</span> + </td> + </tr> + </table> </a-popover> </template> <template slot="expiryTime" slot-scope="text, client, index"> - <template v-if="client.expiryTime > 0"> - <a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)"> - [[ DateUtil.formatMillis(client._expiryTime) ]] - </a-tag> + <template v-if="client.expiryTime !=0 && client.reset >0"> + <a-popover :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> + <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span> + </template> + <table> + <tr> + <td width="80px" style="margin:0; text-align: right;font-size: 1em;"> + [[ remainedDays(client.expiryTime) ]] + </td> + <td width="120px" class="infinite-bar"> + <a-progress :show-info="false" + :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''" + :percent="expireProgress(client.expiryTime, client.reset)"/> + </td> + <td width="60px">[[ client.reset + "d" ]]</td> + </tr> + </table> + </a-popover> </template> - <a-tag v-else-if="client.expiryTime < 0" color="cyan"> - [[ client._expiryTime ]] {{ i18n "pages.client.days" }} - </a-tag> - <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> + <template v-else> + <a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> + <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span> + </template> + <a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> + [[ remainedDays(client.expiryTime) ]] + </a-tag> + </a-popover> + <a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: 0;" class="infinite-tag">∞</a-tag> + </template> +</template> +<template slot="actionMenu" slot-scope="text, client, index"> + <a-dropdown :trigger="['click']"> + <a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon> + <a-menu slot="overlay" :theme="themeSwitcher.currentTheme"> + <a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);"> + <a-icon style="font-size: 14px;" type="qrcode"></a-icon> + {{ i18n "qrCode" }} + </a-menu-item> + <a-menu-item @click="openEditClient(record.id,client);"> + <a-icon style="font-size: 14px;" type="edit"></a-icon> + {{ i18n "pages.client.edit" }} + </a-menu-item> + <a-menu-item @click="showInfo(record.id,client);"> + <a-icon style="font-size: 14px;" type="info-circle"></a-icon> + {{ i18n "info" }} + </a-menu-item> + <a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"> + <a-icon style="font-size: 14px;" type="retweet"></a-icon> + {{ i18n "pages.inbounds.resetTraffic" }} + </a-menu-item> + <a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)"> + <a-icon style="font-size: 14px;" type="delete"></a-icon> + <span style="color: #FF4D4F"> {{ i18n "delete"}}</span> + </a-menu-item> + <a-menu-item> + <a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)"> + </a-switch> + {{ i18n "enable"}} + </a-menu-item> + </a-menu> + </a-dropdown> +</template> +<template slot="info" slot-scope="text, client, index"> + <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click"> + <template slot="content"> + <table> + <tr> + <td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td> + </tr> + <tr> + <td width="80px" style="margin:0; text-align: right;font-size: 1em;"> + [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] + </td> + <td width="120px" v-if="!client.enable"> + <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" + :show-info="false" + :percent="statsProgress(record, client.email)"/> + </td> + <td width="120px" v-else-if="client.totalGB > 0"> + <a-popover :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content" v-if="client.email"> + <table cellpadding="2" width="100%"> + <tr> + <td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td> + <td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td> + </tr> + <tr> + <td>{{ i18n "remained" }}</td> + <td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td> + </tr> + </table> + </template> + <a-progress :stroke-color="statsColor(record, client.email)" + :show-info="false" + :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''" + :percent="statsProgress(record, client.email)"/> + </a-popover> + </td> + <td width="120px" v-else class="infinite-bar"> + <a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" + :show-info="false" + :status="isClientOnline(client.email)? 'active' : ''" + :percent="100"></a-progress> + </td> + <td width="80px"> + <template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template> + <span v-else style="font-weight: 100;font-size: 14pt;">∞</span> + </td> + </tr> + <tr> + <td colspan="3" style="text-align: center;"> + <a-divider style="margin: 0; border-collapse: separate;"></a-divider> + {{ i18n "pages.inbounds.expireDate" }} + </td> + </tr> + <tr> + <template v-if="client.expiryTime !=0 && client.reset >0"> + <td width="80px" style="margin:0; text-align: right;font-size: 1em;"> + [[ remainedDays(client.expiryTime) ]] + </td> + <td width="120px" class="infinite-bar"> + <a-popover :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> + <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span> + </template> + <a-progress :show-info="false" + :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''" + :percent="expireProgress(client.expiryTime, client.reset)"/> + </a-popover> + </td> + <td width="60px">[[ client.reset + "d" ]]</td> + </template> + <template v-else> + <td colspan="3" style="text-align: center;"> + <a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span> + <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span> + </template> + <a-tag style="min-width: 50px; border: none;" + :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> + [[ remainedDays(client.expiryTime) ]] + </a-tag> + </a-popover> + <a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">∞</a-tag> + </template> + </td> + </tr> + </table> + </template> + <a-badge> + <a-icon v-if="!client.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon> + <a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"><a-icon type="solution"></a-icon></a-button> + </a-badge> + </a-popover> </template> {{end}} \ No newline at end of file diff --git a/web/html/xui/inbound_info_modal.html b/web/html/xui/inbound_info_modal.html index a82451b3..ba4ec980 100644 --- a/web/html/xui/inbound_info_modal.html +++ b/web/html/xui/inbound_info_modal.html @@ -3,7 +3,7 @@ v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' :closable="true" :mask-closable="true" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" :footer="null" width="600px" > @@ -315,7 +315,7 @@ if (infoModal.clientStats) { return infoModal.clientStats.enable; } - return infoModal.dbInbound.isEnable; + return true; }, get isEnable() { if (infoModal.clientSettings) { diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index a18d7731..99cb05fe 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -1,7 +1,7 @@ {{define "inboundModal"}} <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'> {{template "form/inbound"}} </a-modal> diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 4e1873f8..27612917 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -8,27 +8,56 @@ } } + @media (max-width: 768px) { + .ant-card-body { + padding: .5rem; + } + } + .ant-col-sm-24 { - margin-top: 10px; + margin: 0.5rem -2rem 0.5rem 2rem; } tr.hideExpandIcon .ant-table-row-expand-icon { display: none; } + .infinite-tag { + padding: 0 5px; + border-radius: 2rem; + min-width: 50px; + } + .infinite-bar .ant-progress-inner .ant-progress-bg { + background-color: #F2EAF1; + border: #D5BED2 solid 1px; + } + .dark .infinite-bar .ant-progress-inner .ant-progress-bg { + background-color: #3c1536; + border: #7a316f solid 1px; + } + .ant-collapse { + margin: 5px 0; + } + .online-animation .ant-badge-status-dot { + animation: 1.2s ease infinite normal none running onlineAnimation; + } + @keyframes onlineAnimation { + 0%, 50%, 100% { transform: scale(1); opacity: 1; } + 10% { transform: scale(1.5); opacity: .2; } + } </style> <body> -<a-layout id="app" v-cloak> +<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> {{ template "commonSider" . }} - <a-layout id="content-layout" :style="themeSwitcher.bgStyle"> + <a-layout id="content-layout"> <a-layout-content> - <a-spin :spinning="spinning" :delay="500" tip="loading"> + <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> <transition name="list" appear> <a-tag v-if="false" color="red" style="margin-bottom: 10px"> Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information </a-tag> </transition> <transition name="list" appear> - <a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <a-row> <a-col :xs="24" :sm="24" :lg="12"> {{ i18n "pages.inbounds.totalDownUp" }}: @@ -45,36 +74,46 @@ <a-col :xs="24" :sm="24" :lg="12"> {{ i18n "clients" }}: <a-tag color="green">[[ total.clients ]]</a-tag> - <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass"> + <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p> </template> <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag> </a-popover> - <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass"> + <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p> </template> <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag> </a-popover> - <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass"> + <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p> </template> <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag> </a-popover> + <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p> + </template> + <a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag> + </a-popover> </a-col> </a-row> </a-card> </transition> <transition name="list" appear> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <div slot="title"> <a-row> - <a-col :xs="24" :sm="24" :lg="12"> - <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button> + <a-col :xs="12" :sm="12" :lg="12"> + <a-button type="primary" icon="plus" @click="openAddInbound"> + <template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template> + </a-button> <a-dropdown :trigger="['click']"> - <a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button> + <a-button type="primary" icon="menu"> + <template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template> + </a-button> <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme"> <a-menu-item key="export"> <a-icon type="export"></a-icon> @@ -95,12 +134,12 @@ </a-menu> </a-dropdown> </a-col> - <a-col :xs="24" :sm="24" :lg="12" style="text-align: right;"> + <a-col :xs="12" :sm="12" :lg="12" style="text-align: right;"> <a-select v-model="refreshInterval" style="width: 65px;" v-if="isRefreshEnabled" @change="changeRefreshInterval" - :dropdown-class-name="themeSwitcher.darkCardClass"> + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option> </a-select> <a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon> @@ -108,26 +147,32 @@ </a-col> </a-row> </div> - <a-switch v-model="enableFilter" - checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}' - @change="toggleFilter" style="margin-right: 10px;"> - </a-switch> - <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> - <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"> - <a-radio-button value="">{{ i18n "none" }}</a-radio-button> - <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button> - <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button> - <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button> - </a-radio-group> - <a-table :columns="columns" :row-key="dbInbound => dbInbound.id" + <div style="display: flex; align-items: center; justify-content: flex-start;"> + <a-switch v-model="enableFilter" + style="margin-right: .5rem;" + @change="toggleFilter"> + <a-icon slot="checkedChildren" type="search"></a-icon> + <a-icon slot="unCheckedChildren" type="filter"></a-icon> + </a-switch> + <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input> + <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''"> + <a-radio-button value="">{{ i18n "none" }}</a-radio-button> + <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button> + <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button> + <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button> + <a-radio-button value="online">{{ i18n "online" }}</a-radio-button> + </a-radio-group> + </div> + <a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id" :data-source="searchedInbounds" - :loading="spinning" :scroll="{ x: 1200 }" + :scroll="isMobile ? {} : { x: 1000 }" :pagination="false" :expand-icon-as-cell="false" :expand-row-by-click="false" :expand-icon-column-index="0" - :row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')" - style="margin-top: 20px" + :indent-size="0" + :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')" + style="margin-top: 10px" @change="() => getDBInbounds()"> <template slot="action" slot-scope="text, dbInbound"> <a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon> @@ -142,7 +187,7 @@ <a-icon type="qrcode"></a-icon> {{ i18n "qrCode" }} </a-menu-item> - <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser)"> + <template v-if="dbInbound.isMultiUser()"> <a-menu-item key="addClient"> <a-icon type="user-add"></a-icon> {{ i18n "pages.client.add"}} @@ -181,43 +226,53 @@ <a-icon type="delete"></a-icon> {{ i18n "delete"}} </span> </a-menu-item> + <a-menu-item v-if="isMobile"> + <a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch> + {{ i18n "pages.inbounds.enable" }} + </a-menu-item> </a-menu> </a-dropdown> </template> <template slot="protocol" slot-scope="text, dbInbound"> - <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag> + <a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag> - <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag> - <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag> - <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag> + <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag> + <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="blue">XTLS</a-tag> + <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag> </template> </template> <template slot="clients" slot-scope="text, dbInbound"> <template v-if="clientCount[dbInbound.id]"> <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> - <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass"> + <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> </template> <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> </a-popover> - <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass"> + <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> </template> <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> </a-popover> - <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass"> + <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> </template> <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag> </a-popover> + <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p> + </template> + <a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag> + </a-popover> </template> </template> <template slot="traffic" slot-scope="text, dbInbound"> - <a-popover :overlay-class-name="themeSwitcher.darkClass"> + <a-popover :overlay-class-name="themeSwitcher.currentTheme"> <template slot="content"> <table cellpadding="2" width="100%"> <tr> @@ -230,7 +285,7 @@ </tr> </table> </template> - <a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'"> + <a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)"> [[ sizeFormat(dbInbound.up + dbInbound.down) ]] / <template v-if="dbInbound.total > 0"> [[ sizeFormat(dbInbound.total) ]] @@ -245,35 +300,117 @@ <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch> </template> <template slot="expiryTime" slot-scope="text, dbInbound"> - <template v-if="dbInbound.expiryTime > 0"> - <a-tag v-if="dbInbound.isExpiry" color="red"> + <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> [[ DateUtil.formatMillis(dbInbound.expiryTime) ]] + </template> + <a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)"> + [[ remainedDays(dbInbound._expiryTime) ]] </a-tag> - <a-tag v-else color="blue"> - [[ DateUtil.formatMillis(dbInbound.expiryTime) ]] - </a-tag> - </template> - <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> + </a-popover> + <a-tag v-else color="purple" class="infinite-tag">∞</a-tag> + </template> + <template slot="info" slot-scope="text, dbInbound"> + <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click"> + <template slot="content"> + <table cellpadding="2"> + <tr> + <td>{{ i18n "pages.inbounds.protocol" }}</td> + <td> + <a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag> + <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> + <a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag> + <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag> + <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag> + </template> + </td> + </tr> + <tr> + <td>{{ i18n "pages.inbounds.port" }}</td> + <td><a-tag>[[ dbInbound.port ]]</a-tag></td> + </tr> + <tr v-if="clientCount[dbInbound.id]"> + <td>{{ i18n "clients" }}</td> + <td> + <a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag> + <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> + </template> + <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> + </a-popover> + <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> + </template> + <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> + </a-popover> + <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> + </template> + <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag> + </a-popover> + <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p> + </template> + <a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag> + </a-popover> + </td> + </tr> + <tr> + <td>{{ i18n "pages.inbounds.traffic" }}</td> + <td> + <a-popover :overlay-class-name="themeSwitcher.currentTheme"> + <template slot="content"> + <table cellpadding="2" width="100%"> + <tr> + <td>↑[[ sizeFormat(dbInbound.up) ]]</td> + <td>↓[[ sizeFormat(dbInbound.down) ]]</td> + </tr> + <tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total"> + <td>{{ i18n "remained" }}</td> + <td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td> + </tr> + </table> + </template> + <a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)"> + [[ sizeFormat(dbInbound.up + dbInbound.down) ]] / + <template v-if="dbInbound.total > 0"> + [[ sizeFormat(dbInbound.total) ]] + </template> + <template v-else>∞</template> + </a-tag> + </a-popover> + </td> + </tr> + <tr> + <td>{{ i18n "pages.inbounds.expireDate" }}</td> + <td> + <a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'"> + [[ DateUtil.formatMillis(dbInbound.expiryTime) ]] + </a-tag> + <a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">∞</a-tag> + </td> + </tr> + </table> + </template> + <a-badge> + <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon> + <a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"> + <a-icon type="info"></a-icon> + </a-button> + </a-badge> + </a-popover> </template> <template slot="expandedRowRender" slot-scope="record"> <a-table - v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)" :row-key="client => client.id" - :columns="innerColumns" + :columns="isMobile ? innerMobileColumns : innerColumns" :data-source="getInboundClients(record)" :pagination="false" - style="margin-left: 20px;" - > - {{template "client_table"}} - </a-table> - <a-table - v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser" - :row-key="client => client.id" - :columns="innerTrojanColumns" - :data-source="getInboundClients(record)" - :pagination="false" - style="margin-left: 20px;" - > + :style="isMobile ? 'margin: -16px -5px -17px;' : 'margin-left: 10px;'"> {{template "client_table"}} </a-table> </template> @@ -292,6 +429,7 @@ align: 'right', dataIndex: "id", width: 30, + responsive: ["xs"], }, { title: '{{ i18n "pages.inbounds.operate" }}', align: 'center', @@ -320,7 +458,7 @@ }, { title: '{{ i18n "clients" }}', align: 'left', - width: 40, + width: 50, scopedSlots: { customRender: 'clients' }, }, { title: '{{ i18n "pages.inbounds.traffic" }}', @@ -330,26 +468,46 @@ }, { title: '{{ i18n "pages.inbounds.expireDate" }}', align: 'center', - width: 80, + width: 40, scopedSlots: { customRender: 'expiryTime' }, }]; + const mobileColums = [{ + title: "ID", + align: 'right', + dataIndex: "id", + width: 10, + responsive: ["s"], + }, { + title: '{{ i18n "pages.inbounds.operate" }}', + align: 'center', + width: 25, + scopedSlots: { customRender: 'action' }, + }, { + title: '{{ i18n "pages.inbounds.remark" }}', + align: 'left', + width: 70, + dataIndex: "remark", + }, { + title: '{{ i18n "pages.inbounds.info" }}', + align: 'center', + width: 10, + scopedSlots: { customRender: 'info' }, + }]; + const innerColumns = [ - { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, - { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } }, + { title: '{{ i18n "pages.inbounds.operate" }}', width: 50, scopedSlots: { customRender: 'actions' } }, + { title: '{{ i18n "pages.inbounds.enable" }}', width: 20, scopedSlots: { customRender: 'enable' } }, + { title: '{{ i18n "online" }}', width: 20, scopedSlots: { customRender: 'online' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, - { title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } }, - { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } }, - { title: 'UUID', width: 120, dataIndex: "id" }, + { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } }, + { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } }, ]; - const innerTrojanColumns = [ - { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, - { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } }, - { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, - { title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } }, - { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } }, - { title: '{{ i18n "password" }}', width: 170, dataIndex: "password" }, + const innerMobileColumns = [ + { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } }, + { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } }, + { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } }, ]; const app = new Vue({ @@ -370,6 +528,7 @@ defaultCert: '', defaultKey: '', clientCount: [], + onlineClients: [], isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, refreshing: false, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, @@ -380,7 +539,8 @@ domain: '', tls: false }, - tgBotEnable: false + tgBotEnable: false, + isMobile: window.innerWidth <= 768, }, methods: { loading(spinning = true) { @@ -393,11 +553,19 @@ this.refreshing = false; return; } + await this.getOnlineUsers(); this.setInbounds(msg.obj); setTimeout(() => { this.refreshing = false; }, 500); }, + async getOnlineUsers() { + const msg = await HttpUtil.post('/panel/inbound/onlines'); + if (!msg.success) { + return; + } + this.onlineClients = msg.obj != null ? msg.obj : []; + }, async getDefaultSettings() { const msg = await HttpUtil.post('/panel/setting/defaultSettings'); if (!msg.success) { @@ -441,7 +609,7 @@ } }, getClientCounts(dbInbound, inbound) { - let clientCount = 0, active = [], deactive = [], depleted = [], expiring = []; + let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = []; clients = this.getClients(dbInbound.protocol, inbound.settings); clientStats = dbInbound.clientStats now = new Date().getTime() @@ -450,6 +618,7 @@ if (dbInbound.enable) { clients.forEach(client => { client.enable ? active.push(client.email) : deactive.push(client.email); + if(this.isClientOnline(client.email)) online.push(client.email); }); clientStats.forEach(client => { if (!client.enable) { @@ -471,6 +640,7 @@ deactive: deactive, depleted: depleted, expiring: expiring, + online: online, }; }, searchInbounds(key) { @@ -547,10 +717,10 @@ clickAction(action, dbInbound) { switch (action.key) { case "qrcode": - this.showQrcode(dbInbound); + this.showQrcode(dbInbound.id); break; case "showInfo": - this.showInfo(dbInbound); + this.showInfo(dbInbound.id); break; case "edit": this.openEditInbound(dbInbound.id); @@ -586,6 +756,7 @@ title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"', content: '{{ i18n "pages.inbounds.cloneInboundContent"}}', okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}', + class: themeSwitcher.currentTheme, cancelText: '{{ i18n "cancel" }}', onOk: () => { const baseInbound = dbInbound.toInbound(); @@ -752,7 +923,7 @@ this.$confirm({ title: '{{ i18n "pages.inbounds.resetTraffic"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => { @@ -767,23 +938,27 @@ this.$confirm({ title: '{{ i18n "pages.inbounds.deleteInbound"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, okText: '{{ i18n "delete"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/del/' + dbInboundId), }); }, - delClient(dbInboundId, client) { + delClient(dbInboundId, client,confirmation = true) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); clientId = this.getClientId(dbInbound.protocol, client); - this.$confirm({ - title: '{{ i18n "pages.inbounds.deleteInbound"}}', - content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', - class: themeSwitcher.darkCardClass, - okText: '{{ i18n "delete"}}', - cancelText: '{{ i18n "cancel"}}', - onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`), - }); + if (confirmation){ + this.$confirm({ + title: '{{ i18n "pages.inbounds.deleteClient"}}', + content: '{{ i18n "pages.inbounds.deleteClientContent"}}', + class: themeSwitcher.currentTheme, + okText: '{{ i18n "delete"}}', + cancelText: '{{ i18n "cancel"}}', + onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`), + }); + } else { + this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`); + } }, getClients(protocol, clientSettings) { switch (protocol) { @@ -860,21 +1035,25 @@ return dbInbound.toInbound().settings.shadowsockses; } }, - resetClientTraffic(client, dbInboundId) { - this.$confirm({ - title: '{{ i18n "pages.inbounds.resetTraffic"}}', - content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', - class: themeSwitcher.darkCardClass, - okText: '{{ i18n "reset"}}', - cancelText: '{{ i18n "cancel"}}', - onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email), - }) + resetClientTraffic(client, dbInboundId, confirmation = true) { + if (confirmation){ + this.$confirm({ + title: '{{ i18n "pages.inbounds.resetTraffic"}}', + content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', + class: themeSwitcher.currentTheme, + okText: '{{ i18n "reset"}}', + cancelText: '{{ i18n "cancel"}}', + onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email), + }) + } else { + this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email); + } }, resetAllTraffic() { this.$confirm({ title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}', content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/resetAllTraffics'), @@ -884,7 +1063,7 @@ this.$confirm({ title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}', content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId), @@ -894,36 +1073,98 @@ this.$confirm({ title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}', content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId), }) }, isExpiry(dbInbound, index) { - return dbInbound.toInbound().isExpiry(index) + return dbInbound.toInbound().isExpiry(index); }, getUpStats(dbInbound, email) { - if (email.length == 0) return 0 - clientStats = dbInbound.clientStats.find(stats => stats.email === email) - return clientStats ? clientStats.up : 0 + if (email.length == 0) return 0; + clientStats = dbInbound.clientStats.find(stats => stats.email === email); + return clientStats ? clientStats.up : 0; }, getDownStats(dbInbound, email) { - if (email.length == 0) return 0 - clientStats = dbInbound.clientStats.find(stats => stats.email === email) - return clientStats ? clientStats.down : 0 + if (email.length == 0) return 0; + clientStats = dbInbound.clientStats.find(stats => stats.email === email); + return clientStats ? clientStats.down : 0; + }, + getSumStats(dbInbound, email) { + if (email.length == 0) return 0; + clientStats = dbInbound.clientStats.find(stats => stats.email === email); + return clientStats ? clientStats.up + clientStats.down : 0; + }, + getRemStats(dbInbound, email) { + if (email.length == 0) return 0; + clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!clientStats) return 0; + remained = clientStats.totalGB - (clientStats.up + clientStats.down); + return remained>0 ? remained : 0; }, statsColor(dbInbound, email) { - if(email.length == 0) return 'blue'; + if (email.length == 0) return '#0e49b5'; clientStats = dbInbound.clientStats.find(stats => stats.email === email); - return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); + switch (true) { + case !clientStats: + return "#0e49b5"; + case clientStats.up + clientStats.down < clientStats.total - app.trafficDiff: + return "#0e49b5"; + case clientStats.up + clientStats.down < clientStats.total: + return "#FFA031"; + default: + return "#E04141"; + } + }, + statsProgress(dbInbound, email) { + if (email.length == 0) return 100; + clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!clientStats) return 0; + if (clientStats.total == 0) return 100; + return 100*(clientStats.down + clientStats.up)/clientStats.total; + }, + expireProgress(expTime, reset) { + now = new Date().getTime(); + remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000; + resetSeconds = reset * 86400; + if (remainedSeconds >= resetSeconds) return 0; + return 100*(1-(remainedSeconds/resetSeconds)); + }, + remainedDays(expTime){ + if (expTime == 0) return null; + if (expTime < 0) return formatSecond(expTime/-1000); + now = new Date().getTime(); + if (expTime < now) return '{{ i18n "depleted" }}'; + return formatSecond((expTime-now)/1000); + }, + statsExpColor(dbInbound, email){ + if (email.length == 0) return '#7a316f'; + clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!clientStats) return '#7a316f'; + statsColor = usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); + expColor = usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); + switch (true) { + case statsColor == "red" || expColor == "red": + return "#E04141"; + case statsColor == "orange" || expColor == "orange": + return "#FFA031"; + case statsColor == "blue" || expColor == "blue": + return "#0e49b5"; + default: + return "#7a316f"; + } }, isClientEnabled(dbInbound, email) { - clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null - return clientStats ? clientStats['enable'] : true + clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null; + return clientStats ? clientStats['enable'] : true; + }, + isClientOnline(email) { + return this.onlineClients.includes(email); }, isRemovable(dbInbound_id) { - return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1 + return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1; }, inboundLinks(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); @@ -933,7 +1174,7 @@ exportAllLinks() { let copyText = ''; for (const dbInbound of this.dbInbounds) { - copyText += dbInbound.genInboundLinks + copyText += dbInbound.genInboundLinks; } txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds'); }, @@ -963,6 +1204,9 @@ this.spinning = false; } }, + onResize() { + this.isMobile = window.innerWidth <= 768; + } }, watch: { searchKey: debounce(function (newVal) { @@ -970,6 +1214,8 @@ }, 500) }, mounted() { + window.addEventListener('resize', this.onResize); + this.onResize(); this.loading(); this.getDefaultSettings(); if (this.isRefreshEnabled) { diff --git a/web/html/xui/index.html b/web/html/xui/index.html index 62e60297..cafdb8f9 100644 --- a/web/html/xui/index.html +++ b/web/html/xui/index.html @@ -6,6 +6,9 @@ .ant-layout-content { margin: 24px 16px; } + .ant-card-hoverable { + margin-inline: 0.3rem; + } } .ant-col-sm-24 { @@ -18,21 +21,20 @@ </style> <body> -<a-layout id="app" v-cloak> +<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> {{ template "commonSider" . }} - <a-layout id="content-layout" :style="themeSwitcher.bgStyle"> + <a-layout id="content-layout"> <a-layout-content> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <transition name="list" appear> <a-row> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <a-row> <a-col :sm="24" :md="12"> <a-row> <a-col :span="12" style="text-align: center"> <a-progress type="dashboard" status="normal" :stroke-color="status.cpu.color" - :class="themeSwitcher.darkCardClass" :percent="status.cpu.percent"></a-progress> <div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div> <div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div> @@ -40,7 +42,6 @@ <a-col :span="12" style="text-align: center"> <a-progress type="dashboard" status="normal" :stroke-color="status.mem.color" - :class="themeSwitcher.darkCardClass" :percent="status.mem.percent"></a-progress> <div> {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] @@ -53,7 +54,6 @@ <a-col :span="12" style="text-align: center"> <a-progress type="dashboard" status="normal" :stroke-color="status.swap.color" - :class="themeSwitcher.darkCardClass" :percent="status.swap.percent"></a-progress> <div> Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] @@ -62,7 +62,6 @@ <a-col :span="12" style="text-align: center"> <a-progress type="dashboard" status="normal" :stroke-color="status.disk.color" - :class="themeSwitcher.darkCardClass" :percent="status.disk.percent"></a-progress> <div> {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] @@ -77,22 +76,22 @@ <transition name="list" appear> <a-row> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> 3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a> Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag> <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a> </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> {{ i18n "menu.link" }}: - <a-tag color="blue" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> - <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> - <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> + <a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> + <a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> + <a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> {{ i18n "pages.index.xrayStatus" }}: <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag> <a-tooltip v-if="status.xray.state === State.Error"> @@ -101,13 +100,13 @@ </template> <a-icon type="question-circle" theme="filled"></a-icon> </a-tooltip> - <a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag> - <a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag> - <a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag> + <a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag> + <a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag> + <a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag> </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> {{ i18n "pages.index.operationHours" }}: Xray: <a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag> @@ -116,7 +115,7 @@ </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] <a-tooltip> <template slot="title"> @@ -127,7 +126,7 @@ </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> {{ i18n "usage"}}: Memory [[ sizeFormat(status.appStats.mem) ]] - Threads [[ status.appStats.threads ]] @@ -135,7 +134,7 @@ </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <a-row> <a-col :span="12"> IPv4: @@ -159,7 +158,7 @@ </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <a-row> <a-col :span="12"> TCP: [[ status.tcpCount ]] @@ -183,7 +182,7 @@ </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <a-row> <a-col :span="12"> <a-icon type="arrow-up"></a-icon> @@ -209,7 +208,7 @@ </a-card> </a-col> <a-col :sm="24" :md="12"> - <a-card hoverable :class="themeSwitcher.darkCardClass"> + <a-card hoverable> <a-row> <a-col :span="12"> <a-icon type="cloud-upload"></a-icon> @@ -241,12 +240,12 @@ <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true" @ok="() => versionModal.visible = false" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" footer=""> <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2> <template v-for="version, index in versionModal.versions"> - <a-tag :color="index % 2 == 0 ? 'blue' : 'green'" + <a-tag :color="index % 2 == 0 ? 'purple' : 'green'" style="margin: 10px" @click="switchV2rayVersion(version)"> [[ version ]] </a-tag> @@ -255,7 +254,7 @@ <a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs" :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false" - :class="themeSwitcher.darkCardClass" + :class="themeSwitcher.currentTheme" width="800px" footer=""> <a-form layout="inline"> @@ -263,7 +262,7 @@ <a-select v-model="logModal.rows" style="width: 80px" @change="openLogs()" - :dropdown-class-name="themeSwitcher.darkCardClass"> + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="10">10</a-select-option> <a-select-option value="20">20</a-select-option> <a-select-option value="50">50</a-select-option> @@ -274,7 +273,7 @@ <a-select v-model="logModal.level" style="width: 120px" @change="openLogs()" - :dropdown-class-name="themeSwitcher.darkCardClass"> + :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option value="debug">Debug</a-select-option> <a-select-option value="info">Info</a-select-option> <a-select-option value="notice">Notice</a-select-option> @@ -300,12 +299,12 @@ </a-modal> <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title" - :closable="true" :class="themeSwitcher.darkCardClass" + :closable="true" :class="themeSwitcher.currentTheme" @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()"> - <p style="color: inherit; font-size: 16px; padding: 4px 2px;"> - <a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon> - [[ backupModal.description ]] - </p> + <a-alert type="warning" style="margin-bottom: 10px; width: fit-content" + :message="backupModal.description" + show-icon + ></a-alert> <a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;"> <a-button type="primary" @click="exportDatabase()"> [[ backupModal.exportText ]] @@ -346,11 +345,11 @@ get color() { const percent = this.percent; if (percent < 80) { - return '#67C23A'; + return '#0a7557'; } else if (percent < 90) { - return '#E6A23C'; + return '#ffa031'; } else { - return '#F56C6C'; + return '#e04141'; } } } @@ -504,7 +503,7 @@ title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, okText: '{{ i18n "confirm"}}', - class: themeSwitcher.darkCardClass, + class: themeSwitcher.currentTheme, cancelText: '{{ i18n "cancel"}}', onOk: async () => { versionModal.hide(); diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index 1f3b0ebb..7c4fef16 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -8,8 +8,11 @@ } } - .ant-col-sm-24 { - margin-top: 10px; + @media (max-width: 768px) { + .ant-tabs-nav .ant-tabs-tab { + margin: 0; + padding: 12px .5rem; + } } .ant-tabs-bar { @@ -20,10 +23,6 @@ display: block; } - :not(.ant-card-dark)>.ant-tabs-top-bar { - background: white; - } - .alert-msg { color: rgb(194, 117, 18); font-weight: normal; @@ -71,25 +70,31 @@ } </style> <body> -<a-layout id="app" v-cloak> +<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> {{ template "commonSider" . }} - <a-layout id="content-layout" :style="themeSwitcher.bgStyle"> + <a-layout id="content-layout"> <a-layout-content> - <a-spin :spinning="spinning" :delay="500" tip="loading"> + <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-space direction="vertical"> - <a-space direction="horizontal"> - <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> - <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> - </a-space> - <a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass"> + <a-card hoverable style="margin-bottom: .5rem;"> + <a-row> + <a-col :xs="24" :sm="8" style="padding: 4px;"> + <a-space direction="horizontal"> + <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> + <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> + </a-space> + </a-col> + <a-col :xs="24" :sm="16"> + <a-alert type="warning" style="float: right; width: fit-content" + message='{{ i18n "pages.settings.infoDesc" }}' + show-icon + > + </a-col> + </a-row> + </a-card> + <a-tabs default-active-key="1"> <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="alert-msg"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.infoDesc" }} - </h2> - </a-row> - <a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> + <a-list item-layout="horizontal"> <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item> @@ -112,7 +117,7 @@ ref="selectLang" v-model="lang" @change="setLang(lang)" - :dropdown-class-name="themeSwitcher.darkCardClass" + :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%" > <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> @@ -127,9 +132,9 @@ </a-list> </a-tab-pane> <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;"> - <a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.darkCardClass"> + <a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.currentTheme"> <a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'> - <a-form :style="'padding: 20px;' + themeSwitcher.textStyle"> + <a-form style="padding: 20px;"> <a-form-item label='{{ i18n "pages.settings.oldUsername"}}'> <a-input v-model="user.oldUsername" style="max-width: 300px"></a-input> </a-form-item> @@ -148,7 +153,7 @@ </a-form> </a-tab-pane> <a-tab-pane key="sec-2" tab='{{ i18n "pages.settings.security.secret"}}'> - <a-form :style="'padding: 20px;' + themeSwitcher.textStyle"> + <a-form style="padding: 20px;"> <a-list-item style="padding: 20px"> <a-row> <a-col :lg="24" :xl="12"> @@ -183,188 +188,8 @@ </a-tabs> </a-tab-pane> - <a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'> - <a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> - <a-divider style="padding: 20px;">{{ i18n "pages.settings.actions"}}</a-divider> - <a-space direction="horizontal" style="padding: 0px 20px"> - <a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button> - </a-space> - <a-divider style="padding: 20px;">{{ i18n "pages.settings.templates.title"}} </a-divider> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="alert-msg"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.infoDesc" }} - </h2> - </a-row> - <a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;"> - <a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;"> - <a-collapse> - <a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.generalConfigsDesc" }} - </h2> - </a-row> - <a-list-item> - <a-row style="padding: 20px"> - <a-col :lg="24" :xl="12"> - <a-list-item-meta - title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}' - description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/> - </a-col> - <a-col :lg="24" :xl="12"> - <template> - <a-select - v-model="freedomStrategy" - :dropdown-class-name="themeSwitcher.darkCardClass" - style="width: 100%"> - <a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option> - </a-select> - </template> - </a-col> - </a-row> - </a-list-item> - <a-row style="padding: 20px"> - <a-col :lg="24" :xl="12"> - <a-list-item-meta - title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}' - description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/> - </a-col> - <a-col :lg="24" :xl="12"> - <template> - <a-select - v-model="routingStrategy" - :dropdown-class-name="themeSwitcher.darkCardClass" - style="width: 100%"> - <a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option> - </a-select> - </template> - </a-col> - </a-row> - </a-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.blockConfigsDesc" }} - </h2> - </a-row> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpeedtest"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpeedtestDesc"}}' v-model="SpeedTestSettings"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }} - </h2> - </a-row> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.directCountryConfigsDesc" }} - </h2> - </a-row> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }} - </h2> - </a-row> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.warpConfigsDesc" }} - </h2> - </a-row> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item> - <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item> - </a-collapse-panel> - </a-collapse> - </a-tab-pane> - <a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;"> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="collapse-title"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.templates.manualListsDesc" }} - </h2> - </a-row> - <a-collapse> - <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'> - <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'> - <setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'> - <setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'> - <setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'> - <setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'> - <setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item> - </a-collapse-panel> - </a-collapse> - </a-tab-pane> - <a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;"> - <a-collapse> - <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'> - <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'> - <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item> - </a-collapse-panel> - <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'> - <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item> - </a-collapse-panel> - </a-collapse> - </a-tab-pane> - <a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;"> - <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item> - </a-tab-pane> - </a-tabs> - </a-list> - </a-tab-pane> - - <a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="alert-msg"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.infoDesc" }} - </h2> - </a-row> - <a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> + <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'> + <a-list item-layout="horizontal"> <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item> @@ -383,7 +208,7 @@ <a-select ref="selectBotLang" v-model="allSetting.tgLang" - :dropdown-class-name="themeSwitcher.darkCardClass" + :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%" > <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> @@ -397,14 +222,8 @@ </a-list-item> </a-list> </a-tab-pane> - <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'> - <a-row :xs="24" :sm="24" :lg="12"> - <h2 class="alert-msg"> - <a-icon type="warning"></a-icon> - {{ i18n "pages.settings.infoDesc" }} - </h2> - </a-row> - <a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> + <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'> + <a-list item-layout="horizontal"> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item> @@ -440,75 +259,6 @@ saveBtnDisable: true, user: new User(), lang: getLang(), - ipv4Settings: { - tag: "IPv4", - protocol: "freedom", - settings: { - domainStrategy: "UseIPv4" - } - }, - warpSettings: { - tag: "WARP", - protocol: "socks", - settings: { - servers: [ - { - address: "127.0.0.1", - port: 40000 - } - ] - } - }, - directSettings: { - tag: "direct", - protocol: "freedom" - }, - outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"], - routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"], - settingsData: { - protocols: { - bittorrent: ["bittorrent"], - }, - ips: { - local: ["geoip:private"], - cn: ["geoip:cn"], - ir: ["ext:geoip_IR.dat:ir","ext:geoip_IR.dat:arvancloud","ext:geoip_IR.dat:derakcloud","ext:geoip_IR.dat:iranserver"], - ru: ["geoip:ru"], - }, - domains: { - ads: [ - "geosite:category-ads-all", - "ext:geosite_IR.dat:category-ads-all" - ], - speedtest: ["geosite:speedtest"], - openai: ["geosite:openai"], - google: ["geosite:google"], - spotify: ["geosite:spotify"], - netflix: ["geosite:netflix"], - cn: [ - "geosite:cn", - "regexp:.*\\.cn$" - ], - ru: [ - "geosite:category-gov-ru", - "regexp:.*\\.ru$" - ], - ir: [ - "regexp:.*\\.ir$", - "regexp:.*\\.xn--mgba3a4f16a$", // .ایران - "ext:geosite_IR.dat:ir" // have rules to bypass all .ir domains. - ] - }, - familyProtectDNS: { - "servers": [ - "1.1.1.3", // https://developers.cloudflare.com/1.1.1.1/setup/ - "1.0.0.3", - "94.140.14.15", // https://adguard-dns.io/kb/general/dns-providers/ - "94.140.15.16" - ], - "queryStrategy": "UseIPv4" - }, - } }, methods: { loading(spinning = true) { @@ -547,6 +297,7 @@ this.$confirm({ title: '{{ i18n "pages.settings.restartPanel" }}', content: '{{ i18n "pages.settings.restartPanelDesc" }}', + class: themeSwitcher.currentTheme, okText: '{{ i18n "sure" }}', cancelText: '{{ i18n "cancel" }}', onOk: () => resolve(), @@ -558,7 +309,9 @@ if (msg.success) { this.loading(true); await PromiseUtil.sleep(5000); - const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; + var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; + if (host == this.oldAllSetting.webDomain) host = null; + if (port == this.oldAllSetting.webPort) port = null; const isTLS = webCertFile !== "" || webKeyFile !== ""; const url = buildURL({ host, port, isTLS, base, path: "panel/settings" }); window.location.replace(url); @@ -605,83 +358,6 @@ this.user.loginSecret = ""; } }, - async resetXrayConfigToDefault() { - this.loading(true); - const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig"); - this.loading(false); - if (msg.success) { - this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2)); - this.saveBtnDisable = true; - } - }, - syncRulesWithOutbound(tag, setting) { - const newTemplateSettings = {...this.templateSettings}; - const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag); - const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag); - if (!haveRules && outboundIndex >= 0) { - newTemplateSettings.outbounds.splice(outboundIndex, 1); - } - if (haveRules && outboundIndex === -1) { - newTemplateSettings.outbounds.push(setting); - } - this.templateSettings = newTemplateSettings; - }, - templateRuleGetter(routeSettings) { - const { property, outboundTag } = routeSettings; - let result = []; - if (this.templateSettings != null) { - this.templateSettings.routing.rules.forEach( - (routingRule) => { - if ( - routingRule.hasOwnProperty(property) && - routingRule.hasOwnProperty("outboundTag") && - routingRule.outboundTag === outboundTag - ) { - result.push(...routingRule[property]); - } - } - ); - } - return result; - }, - templateRuleSetter(routeSettings) { - const { data, property, outboundTag } = routeSettings; - const oldTemplateSettings = this.templateSettings; - const newTemplateSettings = oldTemplateSettings; - currentProperty = this.templateRuleGetter({ outboundTag, property }) - if (currentProperty.length == 0) { - const propertyRule = { - type: "field", - outboundTag, - [property]: data - }; - newTemplateSettings.routing.rules.push(propertyRule); - } - else { - const newRules = []; - insertedOnce = false; - newTemplateSettings.routing.rules.forEach( - (routingRule) => { - if ( - routingRule.hasOwnProperty(property) && - routingRule.hasOwnProperty("outboundTag") && - routingRule.outboundTag === outboundTag - ) { - if (!insertedOnce && data.length > 0) { - insertedOnce = true; - routingRule[property] = data; - newRules.push(routingRule); - } - } - else { - newRules.push(routingRule); - } - } - ); - newTemplateSettings.routing.rules = newRules; - } - this.templateSettings = newTemplateSettings; - } }, async mounted() { await this.getAllSetting(); @@ -690,429 +366,6 @@ this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); } }, - computed: { - templateSettings: { - get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; }, - set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); }, - }, - inboundSettings: { - get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; }, - set: function (newValue) { - newTemplateSettings = this.templateSettings; - newTemplateSettings.inbounds = JSON.parse(newValue); - this.templateSettings = newTemplateSettings; - }, - }, - outboundSettings: { - get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; }, - set: function (newValue) { - newTemplateSettings = this.templateSettings; - newTemplateSettings.outbounds = JSON.parse(newValue); - this.templateSettings = newTemplateSettings; - }, - }, - routingRuleSettings: { - get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; }, - set: function (newValue) { - newTemplateSettings = this.templateSettings; - newTemplateSettings.routing.rules = JSON.parse(newValue); - this.templateSettings = newTemplateSettings; - }, - }, - freedomStrategy: { - get: function () { - if (!this.templateSettings) return "AsIs"; - freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag); - if (!freedomOutbound) return "AsIs"; - if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs"; - return freedomOutbound.settings.domainStrategy; - }, - set: function (newValue) { - newTemplateSettings = this.templateSettings; - freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag); - if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) { - newTemplateSettings.outbounds[freedomOutboundIndex].settings = {"domainStrategy": newValue}; - } else { - newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue; - } - this.templateSettings = newTemplateSettings; - } - }, - routingStrategy: { - get: function () { - if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs"; - return this.templateSettings.routing.domainStrategy; - }, - set: function (newValue) { - newTemplateSettings = this.templateSettings; - newTemplateSettings.routing.domainStrategy = newValue; - this.templateSettings = newTemplateSettings; - } - }, - blockedIPs: { - get: function () { - return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue }); - } - }, - blockedDomains: { - get: function () { - return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue }); - } - }, - blockedProtocols: { - get: function () { - return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue }); - } - }, - directIPs: { - get: function () { - return this.templateRuleGetter({ outboundTag: "direct", property: "ip" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue }); - this.syncRulesWithOutbound("direct", this.directSettings); - } - }, - directDomains: { - get: function () { - return this.templateRuleGetter({ outboundTag: "direct", property: "domain" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue }); - this.syncRulesWithOutbound("direct", this.directSettings); - } - }, - ipv4Domains: { - get: function () { - return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue }); - this.syncRulesWithOutbound("IPv4", this.ipv4Settings); - } - }, - warpDomains: { - get: function () { - return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); - }, - set: function (newValue) { - this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue }); - this.syncRulesWithOutbound("WARP", this.warpSettings); - } - }, - manualBlockedIPs: { - get: function () { return JSON.stringify(this.blockedIPs, null, 2); }, - set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000) - }, - manualBlockedDomains: { - get: function () { return JSON.stringify(this.blockedDomains, null, 2); }, - set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000) - }, - manualDirectIPs: { - get: function () { return JSON.stringify(this.directIPs, null, 2); }, - set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000) - }, - manualDirectDomains: { - get: function () { return JSON.stringify(this.directDomains, null, 2); }, - set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000) - }, - manualIPv4Domains: { - get: function () { return JSON.stringify(this.ipv4Domains, null, 2); }, - set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000) - }, - manualWARPDomains: { - get: function () { return JSON.stringify(this.warpDomains, null, 2); }, - set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000) - }, - torrentSettings: { - get: function () { - return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols); - }, - set: function (newValue) { - if (newValue) { - this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent]; - } else { - this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data)); - } - }, - }, - privateIpSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs); - }, - set: function (newValue) { - if (newValue) { - this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local]; - } else { - this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data)); - } - }, - }, - AdsSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains); - }, - set: function (newValue) { - if (newValue) { - this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads]; - } else { - this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data)); - } - }, - }, - SpeedTestSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.speedtest, this.blockedDomains); - }, - set: function (newValue) { - if (newValue) { - this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.speedtest]; - } else { - this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.speedtest.includes(data)); - } - }, - }, - familyProtectSettings: { - get: function () { - if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false; - return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers); - }, - set: function (newValue) { - newTemplateSettings = this.templateSettings; - if (newValue) { - newTemplateSettings.dns = this.settingsData.familyProtectDNS; - } else { - delete newTemplateSettings.dns; - } - this.templateSettings = newTemplateSettings; - }, - }, - GoogleIPv4Settings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains); - }, - set: function (newValue) { - if (newValue) { - this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google]; - } else { - this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data)); - } - }, - }, - NetflixIPv4Settings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains); - }, - set: function (newValue) { - if (newValue) { - this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix]; - } else { - this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data)); - } - }, - }, - IRIpSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs); - }, - set: function (newValue) { - if (newValue) { - this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir]; - } else { - this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data)); - } - } - }, - IRDomainSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains); - }, - set: function (newValue) { - if (newValue) { - this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir]; - } else { - this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data)); - } - } - }, - ChinaIpSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs); - }, - set: function (newValue) { - if (newValue) { - this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn]; - } else { - this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data)); - } - } - }, - ChinaDomainSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains); - }, - set: function (newValue) { - if (newValue) { - this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn]; - } else { - this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data)); - } - } - }, - RussiaIpSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs); - }, - set: function (newValue) { - if (newValue) { - this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru]; - } else { - this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data)); - } - } - }, - RussiaDomainSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains); - }, - set: function (newValue) { - if (newValue) { - this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru]; - } else { - this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data)); - } - } - }, - IRIpDirectSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.ir, this.directIPs); - }, - set: function (newValue) { - if (newValue) { - this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir]; - } else { - this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data)); - } - } - }, - IRDomainDirectSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.ir, this.directDomains); - }, - set: function (newValue) { - if (newValue) { - this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir]; - } else { - this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data)); - } - } - }, - ChinaIpDirectSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.cn, this.directIPs); - }, - set: function (newValue) { - if (newValue) { - this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn]; - } else { - this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data)); - } - } - }, - ChinaDomainDirectSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.cn, this.directDomains); - }, - set: function (newValue) { - if (newValue) { - this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn]; - } else { - this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data)); - } - } - }, - RussiaIpDirectSettings: { - get: function () { - return doAllItemsExist(this.settingsData.ips.ru, this.directIPs); - }, - set: function (newValue) { - if (newValue) { - this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru]; - } else { - this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data)); - } - } - }, - RussiaDomainDirectSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.ru, this.directDomains); - }, - set: function (newValue) { - if (newValue) { - this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru]; - } else { - this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data)); - } - } - }, - GoogleWARPSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.google, this.warpDomains); - }, - set: function (newValue) { - if (newValue) { - this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google]; - } else { - this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data)); - } - }, - }, - OpenAIWARPSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains); - }, - set: function (newValue) { - if (newValue) { - this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai]; - } else { - this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data)); - } - }, - }, - NetflixWARPSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains); - }, - set: function (newValue) { - if (newValue) { - this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix]; - } else { - this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data)); - } - }, - }, - SpotifyWARPSettings: { - get: function () { - return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains); - }, - set: function (newValue) { - if (newValue) { - this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify]; - } else { - this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data)); - } - }, - }, - }, }); </script> </body> diff --git a/web/html/xui/xray.html b/web/html/xui/xray.html new file mode 100644 index 00000000..db5aa202 --- /dev/null +++ b/web/html/xui/xray.html @@ -0,0 +1,911 @@ +<!DOCTYPE html> +<html lang="en"> +{{template "head" .}} +<style> + @media (min-width: 769px) { + .ant-layout-content { + margin: 24px 16px; + } + } + + @media (max-width: 768px) { + .ant-tabs-nav .ant-tabs-tab { + margin: 0; + padding: 12px .5rem; + } + } + + .ant-tabs-bar { + margin: 0; + } + + .ant-list-item { + display: block; + } + + .collapse-title { + color: inherit; + font-weight: bold; + font-size: 18px; + padding: 10px 20px; + border-bottom: 2px solid; + } + + .collapse-title > i { + color: inherit; + font-size: 24px; + } +</style> +<body> +<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> + {{ template "commonSider" . }} + <a-layout id="content-layout"> + <a-layout-content> + <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> + <a-space direction="vertical"> + <a-card hoverable style="margin-bottom: .5rem;"> + <a-row> + <a-col :xs="24" :sm="8" style="padding: 4px;"> + <a-space direction="horizontal"> + <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.settings.save" }}</a-button> + <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> + </a-space> + </a-col> + <a-col :xs="24" :sm="16"> + <a-alert type="warning" style="float: right; width: fit-content" + message='{{ i18n "pages.settings.infoDesc" }}' + show-icon + > + </a-col> + </a-row> + </a-card> + <a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.currentTheme" style="padding: 20px 20px;"> + <a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;"> + <a-space direction="horizontal" style="padding: 20px 20px"> + <a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button> + </a-space> + <a-collapse> + <a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.generalConfigsDesc" }} + </h2> + </a-row> + <a-list-item> + <a-row style="padding: 20px"> + <a-col :lg="24" :xl="12"> + <a-list-item-meta + title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}' + description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/> + </a-col> + <a-col :lg="24" :xl="12"> + <template> + <a-select + v-model="freedomStrategy" + :dropdown-class-name="themeSwitcher.currentTheme" + style="width: 100%"> + <a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option> + </a-select> + </template> + </a-col> + </a-row> + </a-list-item> + <a-row style="padding: 20px"> + <a-col :lg="24" :xl="12"> + <a-list-item-meta + title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}' + description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/> + </a-col> + <a-col :lg="24" :xl="12"> + <template> + <a-select + v-model="routingStrategy" + :dropdown-class-name="themeSwitcher.currentTheme" + style="width: 100%"> + <a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option> + </a-select> + </template> + </a-col> + </a-row> + </a-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.blockConfigsDesc" }} + </h2> + </a-row> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpeedtest"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpeedtestDesc"}}' v-model="SpeedTestSettings"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }} + </h2> + </a-row> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.directCountryConfigsDesc" }} + </h2> + </a-row> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }} + </h2> + </a-row> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.warpConfigsDesc" }} + </h2> + </a-row> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item> + <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item> + </a-collapse-panel> + </a-collapse> + </a-tab-pane> + <a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;"> + <a-row :xs="24" :sm="24" :lg="12"> + <h2 class="collapse-title"> + <a-icon type="warning"></a-icon> + {{ i18n "pages.settings.templates.manualListsDesc" }} + </h2> + </a-row> + <a-collapse> + <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'> + <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'> + <setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'> + <setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'> + <setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'> + <setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'> + <setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item> + </a-collapse-panel> + </a-collapse> + </a-tab-pane> + <a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;"> + <a-collapse> + <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'> + <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'> + <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item> + </a-collapse-panel> + <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'> + <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item> + </a-collapse-panel> + </a-collapse> + </a-tab-pane> + <a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;"> + <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="this.xraySetting"></setting-list-item> + </a-tab-pane> + </a-tabs> + </a-space> + </a-spin> + </a-layout-content> + </a-layout> +</a-layout> +{{template "js" .}} +{{template "component/themeSwitcher" .}} +{{template "component/setting"}} +<script> + const app = new Vue({ + delimiters: ['[[', ']]'], + el: '#app', + data: { + siderDrawer, + themeSwitcher, + spinning: false, + oldXraySetting: '', + xraySetting: '', + saveBtnDisable: true, + ipv4Settings: { + tag: "IPv4", + protocol: "freedom", + settings: { + domainStrategy: "UseIPv4" + } + }, + warpSettings: { + tag: "WARP", + protocol: "socks", + settings: { + servers: [ + { + address: "127.0.0.1", + port: 40000 + } + ] + } + }, + directSettings: { + tag: "direct", + protocol: "freedom" + }, + outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"], + routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"], + settingsData: { + protocols: { + bittorrent: ["bittorrent"], + }, + ips: { + local: ["geoip:private"], + cn: ["geoip:cn"], + ir: ["ext:geoip_IR.dat:ir","ext:geoip_IR.dat:arvancloud","ext:geoip_IR.dat:derakcloud","ext:geoip_IR.dat:iranserver"], + ru: ["geoip:ru"], + }, + domains: { + ads: [ + "geosite:category-ads-all", + "ext:geosite_IR.dat:category-ads-all" + ], + speedtest: ["geosite:speedtest"], + openai: ["geosite:openai"], + google: ["geosite:google"], + spotify: ["geosite:spotify"], + netflix: ["geosite:netflix"], + cn: [ + "geosite:cn", + "regexp:.*\\.cn$" + ], + ru: [ + "geosite:category-gov-ru", + "regexp:.*\\.ru$" + ], + ir: [ + "regexp:.*\\.ir$", + "regexp:.*\\.xn--mgba3a4f16a$", // .ایران + "ext:geosite_IR.dat:ir" // have rules to bypass all .ir domains. + ] + }, + familyProtectDNS: { + "servers": [ + "1.1.1.3", // https://developers.cloudflare.com/1.1.1.1/setup/ + "1.0.0.3", + "94.140.14.15", // https://adguard-dns.io/kb/general/dns-providers/ + "94.140.15.16" + ], + "queryStrategy": "UseIPv4" + }, + } + }, + methods: { + loading(spinning = true) { + this.spinning = spinning; + }, + async getXraySetting() { + this.loading(true); + const msg = await HttpUtil.post("/panel/xray/"); + this.loading(false); + if (msg.success) { + this.oldXraySetting = msg.obj; + this.xraySetting = msg.obj; + this.saveBtnDisable = true; + } + }, + async updateXraySetting() { + this.loading(true); + const msg = await HttpUtil.post("/panel/xray/update", {xraySetting : this.xraySetting}); + this.loading(false); + if (msg.success) { + await this.getXraySetting(); + } + }, + async restartPanel() { + await new Promise(resolve => { + this.$confirm({ + title: '{{ i18n "pages.settings.restartPanel" }}', + content: '{{ i18n "pages.settings.restartPanelDesc" }}', + class: themeSwitcher.currentTheme, + okText: '{{ i18n "sure" }}', + cancelText: '{{ i18n "cancel" }}', + onOk: () => resolve(), + }); + }); + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/restartPanel"); + this.loading(false); + if (msg.success) { + this.loading(true); + await PromiseUtil.sleep(5000); + var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.xraySetting; + if (host == this.oldXraySetting.webDomain) host = null; + if (port == this.oldXraySetting.webPort) port = null; + const isTLS = webCertFile !== "" || webKeyFile !== ""; + const url = buildURL({ host, port, isTLS, base, path: "panel/settings" }); + window.location.replace(url); + } + }, + async fetchUserSecret() { + this.loading(true); + const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user); + if (userMessage.success) { + this.user = userMessage.obj; + } + this.loading(false); + }, + async updateSecret() { + this.loading(true); + const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user); + if (msg.success) { + this.user = msg.obj; + window.location.replace(basePath + "logout"); + } + this.loading(false); + await this.updateXraySetting(); + }, + generateRandomString(length) { + var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + let randomString = ""; + for (let i = 0; i < length; i++) { + randomString += chars[Math.floor(Math.random() * chars.length)]; + } + return randomString; + }, + async getNewSecret() { + this.loading(true); + await PromiseUtil.sleep(600); + const newSecret = this.generateRandomString(64); + this.user.loginSecret = newSecret; + document.getElementById("token").textContent = newSecret; + this.loading(false); + }, + async toggleToken(value) { + if (value) { + await this.getNewSecret(); + } else { + this.user.loginSecret = ""; + } + }, + async resetXrayConfigToDefault() { + this.loading(true); + const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig"); + this.loading(false); + if (msg.success) { + this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2)); + this.saveBtnDisable = true; + } + }, + syncRulesWithOutbound(tag, setting) { + const newTemplateSettings = {...this.templateSettings}; + const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag); + const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag); + if (!haveRules && outboundIndex >= 0) { + newTemplateSettings.outbounds.splice(outboundIndex, 1); + } + if (haveRules && outboundIndex === -1) { + newTemplateSettings.outbounds.push(setting); + } + this.templateSettings = newTemplateSettings; + }, + templateRuleGetter(routeSettings) { + const { property, outboundTag } = routeSettings; + let result = []; + if (this.templateSettings != null) { + this.templateSettings.routing.rules.forEach( + (routingRule) => { + if ( + routingRule.hasOwnProperty(property) && + routingRule.hasOwnProperty("outboundTag") && + routingRule.outboundTag === outboundTag + ) { + result.push(...routingRule[property]); + } + } + ); + } + return result; + }, + templateRuleSetter(routeSettings) { + const { data, property, outboundTag } = routeSettings; + const oldTemplateSettings = this.templateSettings; + const newTemplateSettings = oldTemplateSettings; + currentProperty = this.templateRuleGetter({ outboundTag, property }) + if (currentProperty.length == 0) { + const propertyRule = { + type: "field", + outboundTag, + [property]: data + }; + newTemplateSettings.routing.rules.push(propertyRule); + } + else { + const newRules = []; + insertedOnce = false; + newTemplateSettings.routing.rules.forEach( + (routingRule) => { + if ( + routingRule.hasOwnProperty(property) && + routingRule.hasOwnProperty("outboundTag") && + routingRule.outboundTag === outboundTag + ) { + if (!insertedOnce && data.length > 0) { + insertedOnce = true; + routingRule[property] = data; + newRules.push(routingRule); + } + } + else { + newRules.push(routingRule); + } + } + ); + newTemplateSettings.routing.rules = newRules; + } + this.templateSettings = newTemplateSettings; + } + }, + async mounted() { + await this.getXraySetting(); + while (true) { + await PromiseUtil.sleep(600); + this.saveBtnDisable = this.oldXraySetting === this.xraySetting; + } + }, + computed: { + templateSettings: { + get: function () { return this.xraySetting ? JSON.parse(this.xraySetting) : null; }, + set: function (newValue) { this.xraySetting = JSON.stringify(newValue, null, 2); }, + }, + inboundSettings: { + get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; }, + set: function (newValue) { + newTemplateSettings = this.templateSettings; + newTemplateSettings.inbounds = JSON.parse(newValue); + this.templateSettings = newTemplateSettings; + }, + }, + outboundSettings: { + get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; }, + set: function (newValue) { + newTemplateSettings = this.templateSettings; + newTemplateSettings.outbounds = JSON.parse(newValue); + this.templateSettings = newTemplateSettings; + }, + }, + routingRuleSettings: { + get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; }, + set: function (newValue) { + newTemplateSettings = this.templateSettings; + newTemplateSettings.routing.rules = JSON.parse(newValue); + this.templateSettings = newTemplateSettings; + }, + }, + freedomStrategy: { + get: function () { + if (!this.templateSettings) return "AsIs"; + freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag); + if (!freedomOutbound) return "AsIs"; + if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs"; + return freedomOutbound.settings.domainStrategy; + }, + set: function (newValue) { + newTemplateSettings = this.templateSettings; + freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag); + if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) { + newTemplateSettings.outbounds[freedomOutboundIndex].settings = {"domainStrategy": newValue}; + } else { + newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue; + } + this.templateSettings = newTemplateSettings; + } + }, + routingStrategy: { + get: function () { + if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs"; + return this.templateSettings.routing.domainStrategy; + }, + set: function (newValue) { + newTemplateSettings = this.templateSettings; + newTemplateSettings.routing.domainStrategy = newValue; + this.templateSettings = newTemplateSettings; + } + }, + blockedIPs: { + get: function () { + return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue }); + } + }, + blockedDomains: { + get: function () { + return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue }); + } + }, + blockedProtocols: { + get: function () { + return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue }); + } + }, + directIPs: { + get: function () { + return this.templateRuleGetter({ outboundTag: "direct", property: "ip" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue }); + this.syncRulesWithOutbound("direct", this.directSettings); + } + }, + directDomains: { + get: function () { + return this.templateRuleGetter({ outboundTag: "direct", property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue }); + this.syncRulesWithOutbound("direct", this.directSettings); + } + }, + ipv4Domains: { + get: function () { + return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue }); + this.syncRulesWithOutbound("IPv4", this.ipv4Settings); + } + }, + warpDomains: { + get: function () { + return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" }); + }, + set: function (newValue) { + this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue }); + this.syncRulesWithOutbound("WARP", this.warpSettings); + } + }, + manualBlockedIPs: { + get: function () { return JSON.stringify(this.blockedIPs, null, 2); }, + set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000) + }, + manualBlockedDomains: { + get: function () { return JSON.stringify(this.blockedDomains, null, 2); }, + set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000) + }, + manualDirectIPs: { + get: function () { return JSON.stringify(this.directIPs, null, 2); }, + set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000) + }, + manualDirectDomains: { + get: function () { return JSON.stringify(this.directDomains, null, 2); }, + set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000) + }, + manualIPv4Domains: { + get: function () { return JSON.stringify(this.ipv4Domains, null, 2); }, + set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000) + }, + manualWARPDomains: { + get: function () { return JSON.stringify(this.warpDomains, null, 2); }, + set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000) + }, + torrentSettings: { + get: function () { + return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols); + }, + set: function (newValue) { + if (newValue) { + this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent]; + } else { + this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data)); + } + }, + }, + privateIpSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs); + }, + set: function (newValue) { + if (newValue) { + this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local]; + } else { + this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data)); + } + }, + }, + AdsSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains); + }, + set: function (newValue) { + if (newValue) { + this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads]; + } else { + this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data)); + } + }, + }, + SpeedTestSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.speedtest, this.blockedDomains); + }, + set: function (newValue) { + if (newValue) { + this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.speedtest]; + } else { + this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.speedtest.includes(data)); + } + }, + }, + familyProtectSettings: { + get: function () { + if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false; + return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers); + }, + set: function (newValue) { + newTemplateSettings = this.templateSettings; + if (newValue) { + newTemplateSettings.dns = this.settingsData.familyProtectDNS; + } else { + delete newTemplateSettings.dns; + } + this.templateSettings = newTemplateSettings; + }, + }, + GoogleIPv4Settings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains); + }, + set: function (newValue) { + if (newValue) { + this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google]; + } else { + this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data)); + } + }, + }, + NetflixIPv4Settings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains); + }, + set: function (newValue) { + if (newValue) { + this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix]; + } else { + this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data)); + } + }, + }, + IRIpSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs); + }, + set: function (newValue) { + if (newValue) { + this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir]; + } else { + this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data)); + } + } + }, + IRDomainSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains); + }, + set: function (newValue) { + if (newValue) { + this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir]; + } else { + this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data)); + } + } + }, + ChinaIpSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs); + }, + set: function (newValue) { + if (newValue) { + this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn]; + } else { + this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data)); + } + } + }, + ChinaDomainSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains); + }, + set: function (newValue) { + if (newValue) { + this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn]; + } else { + this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data)); + } + } + }, + RussiaIpSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs); + }, + set: function (newValue) { + if (newValue) { + this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru]; + } else { + this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data)); + } + } + }, + RussiaDomainSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains); + }, + set: function (newValue) { + if (newValue) { + this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru]; + } else { + this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data)); + } + } + }, + IRIpDirectSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.ir, this.directIPs); + }, + set: function (newValue) { + if (newValue) { + this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir]; + } else { + this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data)); + } + } + }, + IRDomainDirectSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.ir, this.directDomains); + }, + set: function (newValue) { + if (newValue) { + this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir]; + } else { + this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data)); + } + } + }, + ChinaIpDirectSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.cn, this.directIPs); + }, + set: function (newValue) { + if (newValue) { + this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn]; + } else { + this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data)); + } + } + }, + ChinaDomainDirectSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.cn, this.directDomains); + }, + set: function (newValue) { + if (newValue) { + this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn]; + } else { + this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data)); + } + } + }, + RussiaIpDirectSettings: { + get: function () { + return doAllItemsExist(this.settingsData.ips.ru, this.directIPs); + }, + set: function (newValue) { + if (newValue) { + this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru]; + } else { + this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data)); + } + } + }, + RussiaDomainDirectSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.ru, this.directDomains); + }, + set: function (newValue) { + if (newValue) { + this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru]; + } else { + this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data)); + } + } + }, + GoogleWARPSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.google, this.warpDomains); + }, + set: function (newValue) { + if (newValue) { + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google]; + } else { + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data)); + } + }, + }, + OpenAIWARPSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains); + }, + set: function (newValue) { + if (newValue) { + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai]; + } else { + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data)); + } + }, + }, + NetflixWARPSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains); + }, + set: function (newValue) { + if (newValue) { + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix]; + } else { + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data)); + } + }, + }, + SpotifyWARPSettings: { + get: function () { + return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains); + }, + set: function (newValue) { + if (newValue) { + this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify]; + } else { + this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data)); + } + }, + }, + }, + }); +</script> +</body> +</html> \ No newline at end of file