3x-ui/web/html/xui/inbounds.html
2023-02-18 19:25:53 +03:30

529 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>
</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);
},
});
},
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>