<!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"> <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;"> <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-row> </a-card> </transition> <transition name="list" appear> <a-card hoverable> <div slot="title"> <a-button type="primary" @click="openAddInbound">Add Inbound</a-button> <a-button type="primary" @click="exportAllLinks" class="copy-btn">Export Links</a-button> </div> <a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input> <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: 25px" @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)"> <a-menu-item v-if="dbInbound.isSS" key="qrcode"> <a-icon type="qrcode"></a-icon> {{ i18n "qrCode" }} </a-menu-item> <a-menu-item key="edit"> <a-icon type="edit"></a-icon> {{ i18n "edit" }} </a-menu-item> <a-menu-item key="resetTraffic"> <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }} </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 color="blue">[[ dbInbound.protocol ]]</a-tag> </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="stream" slot-scope="text, dbInbound, index"> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> <a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag> <a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag> <a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag> </template> <template v-else>{{ i18n "none" }}</template> </template> <template slot="enable" slot-scope="text, dbInbound"> <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></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_row"}} </a-table> <a-table v-else-if="record.protocol === Protocols.TROJAN" :row-key="client => client.id" :columns="innerTrojanColumns" :data-source="getInboundClients(record)" :pagination="false" > {{template "client_row"}} </a-table> <a-table v-else :row-key="client => client.id" :columns="innerOneColumns" :data-source="record" :pagination="false" > {{template "client_row"}} </a-table> </template> </a-table> </a-card> </transition> </a-spin> </a-layout-content> </a-layout> </a-layout> {{template "js" .}} <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: 30, }, { title: '{{ i18n "pages.inbounds.remark" }}', align: 'center', width: 80, dataIndex: "remark", }, { title: '{{ i18n "pages.inbounds.protocol" }}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' }, }, { title: '{{ i18n "pages.inbounds.port" }}', align: 'center', dataIndex: "port", width: 40, }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', align: 'center', width: 150, scopedSlots: { customRender: 'traffic' }, },{ title: '{{ i18n "pages.inbounds.transportConfig" }}', align: 'center', width: 60, scopedSlots: { customRender: 'stream' }, }, { title: '{{ i18n "pages.inbounds.expireDate" }}', align: 'center', width: 80, scopedSlots: { customRender: 'expiryTime' }, }]; const innerColumns = [ { title: '', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: 'UID', width: 120, dataIndex: "id" }, ]; const innerTrojanColumns = [ { title: '', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: 'Password', width: 100, dataIndex: "password" }, ]; const innerOneColumns = [ { title: '', width: 70, scopedSlots: { customRender: 'actions' } }, ]; const app = new Vue({ delimiters: ['[[', ']]'], el: '#app', data: { siderDrawer, spinning: false, inbounds: [], dbInbounds: [], searchKey: '', searchedInbounds: [], }, methods: { loading(spinning=true) { this.spinning = spinning; }, async getDBInbounds() { this.loading(); const msg = await HttpUtil.post('/xui/inbound/list'); this.loading(false); if (!msg.success) { return; } this.setInbounds(msg.obj); }, setInbounds(dbInbounds) { this.inbounds.splice(0); this.dbInbounds.splice(0); this.searchedInbounds.splice(0); for (const inbound of dbInbounds) { const dbInbound = new DBInbound(inbound); this.inbounds.push(dbInbound.toInbound()); this.dbInbounds.push(dbInbound); this.searchedInbounds.push(dbInbound); } }, 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); } }); } }, clickAction(action, dbInbound) { switch (action.key) { case "qrcode": this.showQrcode(dbInbound); break; case "edit": this.openEditInbound(dbInbound.id); break; case "resetTraffic": this.resetTraffic(dbInbound); break; case "delete": this.delInbound(dbInbound); break; } }, openAddInbound() { inModal.show({ title: '{{ i18n "pages.inbounds.addInbound"}}', okText: '{{ i18n "pages.inbounds.addTo"}}', cancelText: '{{ i18n "close" }}', confirm: async (inbound, dbInbound) => { inModal.loading(); await this.addInbound(inbound, dbInbound); inModal.close(); }, isEdit: false }); }, openEditInbound(dbInbound_id) { dbInbound = this.dbInbounds.find(row => row.id === dbInbound_id); const inbound = dbInbound.toInbound(); inModal.show({ title: '{{ i18n "pages.inbounds.modifyInbound"}}', okText: '{{ i18n "pages.inbounds.revise"}}', 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(), streamSettings: inbound.stream.toString(), sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}', }; await this.submit('/xui/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(), streamSettings: inbound.stream.toString(), sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}', }; await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal); }, resetTraffic(dbInbound) { this.$confirm({ title: '{{ i18n "pages.inbounds.resetTraffic"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => { const inbound = dbInbound.toInbound(); dbInbound.up = 0; dbInbound.down = 0; this.updateInbound(inbound, dbInbound); }, }); }, exportAllLinks() { let copyText = ''; for (const dbInbound of this.dbInbounds) { copyText += dbInbound.genInboundLinks } const clipboard = new ClipboardJS('.copy-btn', { text: function () { return copyText; } }); clipboard.on('success', () => { this.$message.success('Export Links succeed'); }); }, delInbound(dbInbound) { this.$confirm({ title: '{{ i18n "pages.inbounds.deleteInbound"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', okText: '{{ i18n "delete"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id), }); }, showQrcode(dbInbound, clientIndex) { const link = dbInbound.genLink(clientIndex); qrModal.show('{{ i18n "qrCode"}}', link, dbInbound); }, showInfo(dbInbound, index) { infoModal.show(dbInbound, index); }, switchEnable(dbInbound) { this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound); }, async submit(url, data, modal) { const msg = await HttpUtil.postWithModal(url, data, modal); 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 } }, resetClientTraffic(client,inbound,event) { this.$confirm({ title: '{{ i18n "pages.inbounds.resetTraffic"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', okText: '{{ i18n "reset"}}', cancelText: '{{ i18n "cancel"}}', onOk: () => { this.resetClTraffic(client,inbound,event); }, }); }, async resetClTraffic(client,inbound,event) { const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email); if (!msg.success) { return; } clientStats = inbound.clientStats if(clientStats.length > 0) { for (const key in clientStats) { if (Object.hasOwnProperty.call(clientStats, key)) { if(clientStats[key]['email'] == client.email){ clientStats[key]['up'] = 0 clientStats[key]['down'] = 0 } } } } }, isExpiry(dbInbound, index) { return dbInbound.toInbound().isExpiry(index) }, getUpStats(dbInbound, email) { clientStats = dbInbound.clientStats if(clientStats.length > 0) { for (const key in clientStats) { if (Object.hasOwnProperty.call(clientStats, key)) { if(clientStats[key]['email'] == email) return clientStats[key]['up'] } } } }, getDownStats(dbInbound, email) { clientStats = dbInbound.clientStats if(clientStats.length > 0) { for (const key in clientStats) { if (Object.hasOwnProperty.call(clientStats, key)) { if(clientStats[key]['email'] == email) return clientStats[key]['down'] } } } }, isTrafficExhausted(dbInbound, email) { clientStats = dbInbound.clientStats if(clientStats.length > 0) { for (const key in clientStats) { if (Object.hasOwnProperty.call(clientStats, key)) { if(clientStats[key]['email'] == email) return clientStats[key]['down']+clientStats[key]['up'] > clientStats[key]['total'] } } } }, isClientEnabled(dbInbound, email) { clientStats = dbInbound.clientStats if(clientStats.length > 0) { for (const key in clientStats) { if (Object.hasOwnProperty.call(clientStats, key)) { if(clientStats[key]['email'] == email) return clientStats[key]['enable'] } } } else{ return true } }, }, watch: { searchKey: debounce(function (newVal) { this.searchInbounds(newVal); }, 500) }, mounted() { this.getDBInbounds(); }, computed: { total() { let down = 0, up = 0; for (let i = 0; i < this.dbInbounds.length; ++i) { down += this.dbInbounds[i].down; up += this.dbInbounds[i].up; } return { down: down, up: up, }; } }, }); </script> {{template "inboundModal"}} {{template "promptModal"}} {{template "qrcodeModal"}} {{template "textModal"}} {{template "inboundInfoModal"}} </body> </html>