<!DOCTYPE html> <html lang="en"> {{template "head" .}} <style> @media (min-width: 769px) { .ant-layout-content { margin: 24px 16px; } } .ant-col-sm-24 { margin-top: 10px; } </style> <body> <a-layout id="app" v-cloak> {{ template "commonSider" . }} <a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout-content> <a-spin :spinning="spinning" :delay="500" tip="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-row> <a-col :xs="24" :sm="24" :lg="12"> {{ i18n "pages.inbounds.totalDownUp" }}: <a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag> </a-col> <a-col :xs="24" :sm="24" :lg="12"> {{ i18n "pages.inbounds.totalUsage" }}: <a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag> </a-col> <a-col :xs="24" :sm="24" :lg="12"> {{ i18n "pages.inbounds.inboundCount" }}: <a-tag color="green">[[ dbInbounds.length ]]</a-tag> </a-col> <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"> <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"> <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"> <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-col> </a-row> </a-card> </transition> <transition name="list" appear> <a-card hoverable :class="themeSwitcher.darkCardClass"> <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-dropdown :trigger="['click']"> <a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button> <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme"> <a-menu-item key="export"> <a-icon type="export"></a-icon> {{ i18n "pages.inbounds.export" }} </a-menu-item> <a-menu-item key="resetInbounds"> <a-icon type="reload"></a-icon> {{ i18n "pages.inbounds.resetAllTraffic" }} </a-menu-item> <a-menu-item key="resetClients"> <a-icon type="file-done"></a-icon> {{ i18n "pages.inbounds.resetAllClientTraffics" }} </a-menu-item> <a-menu-item key="delDepletedClients"> <a-icon type="rest"></a-icon> {{ i18n "pages.inbounds.delDepletedClients" }} </a-menu-item> </a-menu> </a-dropdown> </a-col> <a-col :xs="24" :sm="24" :lg="12" style="text-align: right;"> <a-select v-model="refreshInterval" style="width: 65px;" v-if="isRefreshEnabled" @change="changeRefreshInterval" :dropdown-class-name="themeSwitcher.darkCardClass"> <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> <a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch> </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" :data-source="searchedInbounds" :loading="spinning" :scroll="{ x: 1300 }" :pagination="false" style="margin-top: 20px" @change="() => getDBInbounds()"> <template slot="action" slot-scope="text, dbInbound"> <a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon> <a-dropdown :trigger="['click']"> <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a> <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme"> <a-menu-item key="edit"> <a-icon type="edit"></a-icon> {{ i18n "edit" }} </a-menu-item> <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS"> <a-menu-item key="addClient"> <a-icon type="user-add"></a-icon> {{ i18n "pages.client.add"}} </a-menu-item> <a-menu-item key="addBulkClient"> <a-icon type="usergroup-add"></a-icon> {{ i18n "pages.client.bulk"}} </a-menu-item> <a-menu-item key="resetClients"> <a-icon type="file-done"></a-icon> {{ i18n "pages.inbounds.resetInboundClientTraffics"}} </a-menu-item> <a-menu-item key="export"> <a-icon type="export"></a-icon> {{ i18n "pages.inbounds.export"}} </a-menu-item> <a-menu-item key="delDepletedClients"> <a-icon type="rest"></a-icon> {{ i18n "pages.inbounds.delDepletedClients" }} </a-menu-item> </template> <template v-else> <a-menu-item key="showInfo"> <a-icon type="info-circle"></a-icon> {{ i18n "info"}} </a-menu-item> </template> <a-menu-item key="resetTraffic"> <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }} </a-menu-item> <a-menu-item key="clone"> <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}} </a-menu-item> <a-menu-item key="delete"> <span style="color: #FF4D4F"> <a-icon type="delete"></a-icon> {{ i18n "delete"}} </span> </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> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan"> <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> </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"> <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"> <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"> <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> </template> </template> <template slot="traffic" slot-scope="text, dbInbound"> <a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag> <template v-if="dbInbound.total > 0"> <a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag> <a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag> </template> <a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag> </template> <template slot="enable" slot-scope="text, dbInbound"> <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"> [[ DateUtil.formatMillis(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> </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" :data-source="getInboundClients(record)" :pagination="false" > {{template "client_table"}} </a-table> <a-table v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS" :row-key="client => client.id" :columns="innerTrojanColumns" :data-source="getInboundClients(record)" :pagination="false" > {{template "client_table"}} </a-table> </template> </a-table> </a-card> </transition> </a-spin> </a-layout-content> </a-layout> </a-layout> {{template "js" .}} {{template "component/themeSwitcher" .}} <script> const columns = [{ title: '{{ i18n "pages.inbounds.operate" }}', align: 'center', width: 60, scopedSlots: { customRender: 'action' }, }, { title: '{{ i18n "pages.inbounds.enable" }}', align: 'center', width: 40, scopedSlots: { customRender: 'enable' }, }, { title: "ID", align: 'center', dataIndex: "id", width: 40, }, { title: '{{ i18n "pages.inbounds.remark" }}', align: 'center', width: 80, dataIndex: "remark", }, { title: '{{ i18n "pages.inbounds.port" }}', align: 'center', dataIndex: "port", width: 40, }, { title: '{{ i18n "pages.inbounds.protocol" }}', align: 'left', width: 90, scopedSlots: { customRender: 'protocol' }, }, { title: '{{ i18n "clients" }}', align: 'left', width: 50, scopedSlots: { customRender: 'clients' }, }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', align: 'center', width: 120, scopedSlots: { customRender: 'traffic' }, }, { title: '{{ i18n "pages.inbounds.expireDate" }}', align: 'center', width: 80, scopedSlots: { customRender: 'expiryTime' }, }]; 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.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: 'UID', width: 120, dataIndex: "id" }, ]; 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: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: 'Password', width: 170, dataIndex: "password" }, ]; const app = new Vue({ delimiters: ['[[', ']]'], el: '#app', data: { siderDrawer, themeSwitcher, spinning: false, inbounds: [], dbInbounds: [], searchKey: '', enableFilter: false, filterBy: '', searchedInbounds: [], expireDiff: 0, trafficDiff: 0, defaultCert: '', defaultKey: '', clientCount: {}, isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, refreshing: false, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000 }, methods: { loading(spinning = true) { this.spinning = spinning; }, async getDBInbounds() { this.refreshing = true; const msg = await HttpUtil.post('/panel/inbound/list'); if (!msg.success) { return; } this.setInbounds(msg.obj); setTimeout(() => { this.refreshing = false; }, 500); }, async getDefaultSettings() { const msg = await HttpUtil.post('/panel/setting/defaultSettings'); if (!msg.success) { return; } this.expireDiff = msg.obj.expireDiff * 86400000; this.trafficDiff = msg.obj.trafficDiff * 1073741824; this.defaultCert = msg.obj.defaultCert; this.defaultKey = msg.obj.defaultKey; }, setInbounds(dbInbounds) { this.inbounds.splice(0); this.dbInbounds.splice(0); for (const inbound of dbInbounds) { const dbInbound = new DBInbound(inbound); to_inbound = dbInbound.toInbound() this.inbounds.push(to_inbound); this.dbInbounds.push(dbInbound); if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) { this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound); } } if(this.enableFilter){ this.filterInbounds(); } else { this.searchInbounds(this.searchKey); } }, getClientCounts(dbInbound, inbound) { let clientCount = 0, active = [], deactive = [], depleted = [], expiring = []; clients = this.getClients(dbInbound.protocol, inbound.settings); clientStats = dbInbound.clientStats now = new Date().getTime() if (clients) { clientCount = clients.length; if (dbInbound.enable) { clients.forEach(client => { client.enable ? active.push(client.email) : deactive.push(client.email); }); clientStats.forEach(client => { if (!client.enable) { depleted.push(client.email); } else { if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) || (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email); } }); } else { clients.forEach(client => { deactive.push(client.email); }); } } return { clients: clientCount, active: active, deactive: deactive, depleted: depleted, expiring: expiring, }; }, searchInbounds(key) { if (ObjectUtil.isEmpty(key)) { this.searchedInbounds = this.dbInbounds.slice(); } else { this.searchedInbounds.splice(0, this.searchedInbounds.length); this.dbInbounds.forEach(inbound => { if (ObjectUtil.deepSearch(inbound, key)) { const newInbound = new DBInbound(inbound); const inboundSettings = JSON.parse(inbound.settings); if (inboundSettings.hasOwnProperty('clients')) { const searchedSettings = { "clients": [] }; inboundSettings.clients.forEach(client => { if (ObjectUtil.deepSearch(client, key)) { searchedSettings.clients.push(client); } }); newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings); } this.searchedInbounds.push(newInbound); } }); } }, filterInbounds() { if (ObjectUtil.isEmpty(this.filterBy)) { this.searchedInbounds = this.dbInbounds.slice(); } else { this.searchedInbounds.splice(0, this.searchedInbounds.length); this.dbInbounds.forEach(inbound => { const newInbound = new DBInbound(inbound); const inboundSettings = JSON.parse(inbound.settings); if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){ const list = this.clientCount[inbound.id][this.filterBy]; if (list.length > 0) { const filteredSettings = { "clients": [] }; inboundSettings.clients.forEach(client => { if (list.includes(client.email)) { filteredSettings.clients.push(client); } }); newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings); this.searchedInbounds.push(newInbound); } } }); } }, toggleFilter(){ if(this.enableFilter) { this.searchKey = ''; } else { this.filterBy = ''; this.searchedInbounds = this.dbInbounds.slice(); } }, generalActions(action) { switch (action.key) { case "export": this.exportAllLinks(); break; case "resetInbounds": this.resetAllTraffic(); break; case "resetClients": this.resetAllClientTraffics(-1); break; case "delDepletedClients": this.delDepletedClients(-1) break; } }, clickAction(action, dbInbound) { switch (action.key) { case "qrcode": this.showQrcode(dbInbound); break; case "showInfo": this.showInfo(dbInbound); break; case "edit": this.openEditInbound(dbInbound.id); break; case "addClient": this.openAddClient(dbInbound.id) break; case "addBulkClient": this.openAddBulkClient(dbInbound.id) break; case "export": this.inboundLinks(dbInbound.id); break; case "resetTraffic": this.resetTraffic(dbInbound.id); break; case "resetClients": this.resetAllClientTraffics(dbInbound.id); break; case "clone": this.openCloneInbound(dbInbound); break; case "delete": this.delInbound(dbInbound.id); break; case "delDepletedClients": this.delDepletedClients(dbInbound.id) break; } }, openCloneInbound(dbInbound) { this.$confirm({ title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"', content: '{{ i18n "pages.inbounds.cloneInboundContent"}}', okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}', cancelText: '{{ i18n "cancel" }}', onOk: () => { const baseInbound = dbInbound.toInbound(); dbInbound.up = 0; dbInbound.down = 0; this.cloneInbound(baseInbound, dbInbound); }, }); }, async cloneInbound(baseInbound, dbInbound) { const data = { up: dbInbound.up, down: dbInbound.down, total: dbInbound.total, remark: dbInbound.remark + " - Cloned", enable: dbInbound.enable, expiryTime: dbInbound.expiryTime, listen: '', port: RandomUtil.randomIntRange(10000, 60000), protocol: baseInbound.protocol, settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(), streamSettings: baseInbound.stream.toString(), sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}', }; await this.submit('/panel/inbound/add', data, inModal); }, openAddInbound() { inModal.show({ title: '{{ i18n "pages.inbounds.addInbound"}}', okText: '{{ i18n "pages.inbounds.create"}}', cancelText: '{{ i18n "close" }}', confirm: async (inbound, dbInbound) => { inModal.loading(); await this.addInbound(inbound, dbInbound); inModal.close(); }, isEdit: false }); }, openEditInbound(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); const inbound = dbInbound.toInbound(); inModal.show({ title: '{{ i18n "pages.inbounds.modifyInbound"}}', okText: '{{ i18n "pages.inbounds.update"}}', cancelText: '{{ i18n "close" }}', inbound: inbound, dbInbound: dbInbound, confirm: async (inbound, dbInbound) => { inModal.loading(); await this.updateInbound(inbound, dbInbound); inModal.close(); }, isEdit: true }); }, async addInbound(inbound, dbInbound) { const data = { up: dbInbound.up, down: dbInbound.down, total: dbInbound.total, remark: dbInbound.remark, enable: dbInbound.enable, expiryTime: dbInbound.expiryTime, listen: inbound.listen, port: inbound.port, protocol: inbound.protocol, settings: inbound.settings.toString(), }; if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString(); await this.submit('/panel/inbound/add', data, inModal); }, async updateInbound(inbound, dbInbound) { const data = { up: dbInbound.up, down: dbInbound.down, total: dbInbound.total, remark: dbInbound.remark, enable: dbInbound.enable, expiryTime: dbInbound.expiryTime, listen: inbound.listen, port: inbound.port, protocol: inbound.protocol, settings: inbound.settings.toString(), }; if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString(); await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal); }, openAddClient(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); clientModal.show({ title: '{{ i18n "pages.client.add"}}', okText: '{{ i18n "pages.client.submitAdd"}}', dbInbound: dbInbound, confirm: async (clients, dbInboundId) => { clientModal.loading(); await this.addClient(clients, dbInboundId); clientModal.close(); }, isEdit: false }); }, openAddBulkClient(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); clientsBulkModal.show({ title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark, okText: '{{ i18n "pages.client.bulk"}}', dbInbound: dbInbound, confirm: async (clients, dbInboundId) => { clientsBulkModal.loading(); await this.addClient(clients, dbInboundId); clientsBulkModal.close(); }, }); }, openEditClient(dbInboundId, client) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); clients = this.getInboundClients(dbInbound); index = this.findIndexOfClient(clients, client); clientModal.show({ title: '{{ i18n "pages.client.edit"}}', okText: '{{ i18n "pages.client.submitEdit"}}', dbInbound: dbInbound, index: index, confirm: async (client, dbInboundId, clientId) => { clientModal.loading(); await this.updateClient(client, dbInboundId, clientId); clientModal.close(); }, isEdit: true }); }, findIndexOfClient(clients, client) { firstKey = Object.keys(client)[0]; return clients.findIndex(c => c[firstKey] === client[firstKey]); }, async addClient(clients, dbInboundId) { const data = { id: dbInboundId, settings: '{"clients": [' + clients.toString() + ']}', }; await this.submit(`/panel/inbound/addClient`, data); }, async updateClient(client, dbInboundId, clientId) { const data = { id: dbInboundId, settings: '{"clients": [' + client.toString() + ']}', }; await this.submit(`/panel/inbound/updateClient/${clientId}`, data); }, resetTraffic(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); this.$confirm({ title: '{{ i18n "pages.inbounds.resetTraffic"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', class: themeSwitcher.darkCardClass, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => { const inbound = dbInbound.toInbound(); dbInbound.up = 0; dbInbound.down = 0; this.updateInbound(inbound, dbInbound); }, }); }, delInbound(dbInboundId) { 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/del/' + dbInboundId), }); }, delClient(dbInboundId, client) { 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}`), }); }, getClients(protocol, clientSettings) { switch (protocol) { case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VLESS: return clientSettings.vlesses; case Protocols.TROJAN: return clientSettings.trojans; case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses; default: return null; } }, getClientId(protocol, client) { switch (protocol) { case Protocols.TROJAN: return client.password; case Protocols.SHADOWSOCKS: return client.email; default: return client.id; } }, showQrcode(dbInbound, clientIndex) { const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email; const link = dbInbound.genLink(clientIndex); qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName); }, showInfo(dbInbound, index) { infoModal.show(dbInbound, index); }, switchEnable(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound); }, async switchEnableClient(dbInboundId, client) { this.loading() dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); inbound = dbInbound.toInbound(); clients = this.getClients(dbInbound.protocol, inbound.settings); index = this.findIndexOfClient(clients, client); clients[index].enable = !clients[index].enable; clientId = this.getClientId(dbInbound.protocol, clients[index]); await this.updateClient(clients[index], dbInboundId, clientId); this.loading(false); }, async submit(url, data) { const msg = await HttpUtil.postWithModal(url, data); if (msg.success) { await this.getDBInbounds(); } }, getInboundClients(dbInbound) { if (dbInbound.protocol == Protocols.VLESS) { return dbInbound.toInbound().settings.vlesses; } else if (dbInbound.protocol == Protocols.VMESS) { return dbInbound.toInbound().settings.vmesses; } else if (dbInbound.protocol == Protocols.TROJAN) { return dbInbound.toInbound().settings.trojans; } else if (dbInbound.protocol == Protocols.SHADOWSOCKS) { 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), }) }, resetAllTraffic() { this.$confirm({ title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}', content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}', class: themeSwitcher.darkCardClass, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/resetAllTraffics'), }); }, resetAllClientTraffics(dbInboundId) { 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, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId), }) }, delDepletedClients(dbInboundId) { this.$confirm({ title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}', content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}', class: themeSwitcher.darkCardClass, okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId), }) }, isExpiry(dbInbound, 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 }, getDownStats(dbInbound, email) { if (email.length == 0) return 0 clientStats = dbInbound.clientStats.find(stats => stats.email === email) return clientStats ? clientStats.down : 0 }, statsColor(dbInbound, email) { if(email.length == 0) return 'blue'; clientStats = dbInbound.clientStats.find(stats => stats.email === email); return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); }, isClientEnabled(dbInbound, email) { clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null return clientStats ? clientStats['enable'] : true }, isRemovable(dbInbound_id) { return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1 }, inboundLinks(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark); }, exportAllLinks() { let copyText = ''; for (const dbInbound of this.dbInbounds) { copyText += dbInbound.genInboundLinks } txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds'); }, async startDataRefreshLoop() { while (this.isRefreshEnabled) { try { await this.getDBInbounds(); } catch (e) { console.error(e); } await PromiseUtil.sleep(this.refreshInterval); } }, toggleRefresh() { localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled); if (this.isRefreshEnabled) { this.startDataRefreshLoop(); } }, changeRefreshInterval() { localStorage.setItem("refreshInterval", this.refreshInterval); }, async manualRefresh() { if (!this.refreshing) { this.spinning = true; await this.getDBInbounds(); this.spinning = false; } }, }, watch: { searchKey: debounce(function (newVal) { this.searchInbounds(newVal); }, 500) }, mounted() { this.loading(); this.getDefaultSettings(); if (this.isRefreshEnabled) { this.startDataRefreshLoop(); } else { this.getDBInbounds(); } this.loading(false); }, computed: { total() { let down = 0, up = 0; let clients = 0, deactive = [], depleted = [], expiring = []; this.dbInbounds.forEach(dbInbound => { down += dbInbound.down; up += dbInbound.up; if (this.clientCount[dbInbound.id]) { clients += this.clientCount[dbInbound.id].clients; deactive = deactive.concat(this.clientCount[dbInbound.id].deactive); depleted = depleted.concat(this.clientCount[dbInbound.id].depleted); expiring = expiring.concat(this.clientCount[dbInbound.id].expiring); } }); return { down: down, up: up, clients: clients, deactive: deactive, depleted: depleted, expiring: expiring, }; } }, }); </script> {{template "inboundModal"}} {{template "promptModal"}} {{template "qrcodeModal"}} {{template "textModal"}} {{template "inboundInfoModal"}} {{template "clientsModal"}} {{template "clientsBulkModal"}} </body> </html>