完成xray启动

This commit is contained in:
sprov
2021-05-27 23:04:39 +08:00
parent 56ed8f355c
commit 3cd25ce5ea
42 changed files with 3627 additions and 229 deletions

View File

@@ -9,10 +9,11 @@
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/common.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;

View File

@@ -1,4 +1,4 @@
{{define "promptModel"}}
{{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:ok-text="promptModal.okText" cancel-text="取消">

View File

@@ -0,0 +1,59 @@
{{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" width="300px" :ok-text="qrModal.okText"
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
</a-modal>
<script>
const qrModal = {
title: '',
content: '',
okText: '',
copyText: '',
qrcode: null,
clipboard: null,
visible: false,
show: function (title='', content='', okText='{{ i18n "copy" }}', copyText='') {
this.title = title;
this.content = content;
this.okText = okText;
if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content;
} else {
this.copyText = copyText;
}
this.visible = true;
qrModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
text: () => this.copyText,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
},
close: function () {
this.visible = false;
},
};
const qrModalApp = new Vue({
el: '#qrcode-modal',
data: {
qrModal: qrModal,
},
});
</script>
{{end}}

View File

@@ -0,0 +1,58 @@
{{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
@click="downloader.download(txtModal.fileName, txtModal.content)">
{{ i18n "download" }} [[ txtModal.fileName ]]
</a-button>
<a-input type="textarea" v-model="txtModal.content"
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
</a-modal>
<script>
const txtModal = {
title: '',
content: '',
fileName: '',
qrcode: null,
clipboard: null,
visible: false,
show: function (title='', content='', fileName='') {
this.title = title;
this.content = content;
this.fileName = fileName;
this.visible = true;
textModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
text: () => this.content,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
},
close: function () {
this.visible = false;
},
};
const textModalApp = new Vue({
el: '#text-modal',
data: {
txtModal: txtModal,
},
});
</script>
{{end}}

View File

@@ -3,7 +3,7 @@
<a-icon type="dashboard"></a-icon>
<span>系统状态</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/accounts">
<a-menu-item key="{{ .base_path }}xui/inbounds">
<a-icon type="user"></a-icon>
<span>账号列表</span>
</a-menu-item>

View File

@@ -0,0 +1,535 @@
{{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"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
<!-- base -->
<a-form layout="inline">
<a-form-item label='{{ i18n "remark" }}'>
<a-input v-model.trim="inModal.inbound.remark"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "enable" }}'>
<a-switch v-model="inModal.inbound.enable"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inModal.inbound.protocol" style="width: 160px;"
@change="protocolChange">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
监听 IP
<a-tooltip>
<template slot="title">
不懂请保持默认
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="inModal.inbound.listen"></a-input>
</a-form-item>
<a-form-item label="端口">
<a-input type="number" v-model.number="inModal.inbound.port"></a-input>
</a-form-item>
</a-form>
<!-- vmess settings -->
<a-form v-if="inModal.inbound.protocol === Protocols.VMESS"
layout="inline">
<a-form-item label="id">
<a-input v-model.trim="inModal.inbound.settings.vmesses[0].id"></a-input>
</a-form-item>
<a-form-item label="额外 ID">
<a-input type="number" v-model.number="inModal.inbound.settings.vmesses[0].alterId"></a-input>
</a-form-item>
<a-form-item label="禁用不安全加密">
<a-switch v-model.number="inModal.inbound.settings.disableInsecure"></a-switch>
</a-form-item>
</a-form>
<!-- vless settings -->
<a-form v-if="inModal.inbound.protocol === Protocols.VLESS"
layout="inline">
<a-form-item label="id">
<a-input v-model.trim="inModal.inbound.settings.vlesses[0].id"></a-input>
</a-form-item>
<a-form-item label="flow">
<a-select v-model="inModal.inbound.settings.vlesses[0].flow" style="width: 150px">
<a-select-option value=""></a-select-option>
<a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
<a-form v-if="inModal.inbound.protocol === Protocols.VLESS"
layout="inline">
<a-form-item label="fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inModal.inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inModal.inbound.settings.fallbacks" layout="inline">
<a-form-item>
<a-divider>
fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inModal.inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
</a-form-item>
<a-form-item label="name">
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label="alpn">
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label="path">
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xver">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
</a-form>
<!-- trojan settings -->
<a-form v-if="inModal.inbound.protocol === Protocols.TROJAN"
layout="inline">
<a-form-item label="密码">
<a-input v-model.trim="inModal.inbound.settings.clients[0].password"></a-input>
</a-form-item>
</a-form>
<!-- shadowsocks -->
<a-form v-if="inModal.inbound.protocol === Protocols.SHADOWSOCKS"
layout="inline">
<a-form-item label="加密">
<a-select v-model="inModal.inbound.settings.method" style="width: 165px;">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="密码">
<a-input v-model.trim="inModal.inbound.settings.password"></a-input>
</a-form-item>
<a-form-item label="网络">
<a-select v-model="inModal.inbound.settings.network" style="width: 100px;">
<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>
</a-select>
</a-form-item>
</a-form>
<!-- stream settings -->
<template v-if="inModal.inbound.protocol === Protocols.VMESS
|| inModal.inbound.protocol === Protocols.VLESS
|| inModal.inbound.protocol === Protocols.SHADOWSOCKS">
<!-- select stream network -->
<a-form layout="inline">
<a-form-item label="传输">
<a-select v-model="inModal.inbound.stream.network" @change="streamNetworkChange">
<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>
<a-select-option value="http">http</a-select-option>
<a-select-option value="quic">quic</a-select-option>
</a-select>
</a-form-item>
</a-form>
<!-- vmess tcp -->
<template v-if="inModal.inbound.stream.network === 'tcp'">
<!-- vmess tcp type -->
<a-form layout="inline">
<a-form-item label="http 伪装">
<a-switch
:checked="inModal.inbound.stream.tcp.type === 'http'"
@change="checked => inModal.inbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
</a-form-item>
</a-form>
<!-- vmess tcp request -->
<a-form v-if="inModal.inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form-item label="请求版本">
<a-input v-model.trim="inModal.inbound.stream.tcp.request.version"></a-input>
</a-form-item>
<a-form-item label="请求方法">
<a-input v-model.trim="inModal.inbound.stream.tcp.request.method"></a-input>
</a-form-item>
<a-form-item label="请求路径">
<a-row v-for="(path, index) in inModal.inbound.stream.tcp.request.path">
<a-input v-model.trim="inModal.inbound.stream.tcp.request.path[index]"></a-input>
</a-row>
</a-form-item>
<a-form-item label="请求头">
<a-row>
<a-button size="small"
@click="inModal.inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inModal.inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before="名称"></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before="值">
<template slot="addonAfter">
<a-button size="small"
@click="inModal.inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
<!-- vmess tcp response -->
<a-form v-if="inModal.inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form-item label="响应版本">
<a-input v-model.trim="inModal.inbound.stream.tcp.response.version"></a-input>
</a-form-item>
<a-form-item label="响应状态">
<a-input v-model.trim="inModal.inbound.stream.tcp.response.status"></a-input>
</a-form-item>
<a-form-item label="响应状态说明">
<a-input v-model.trim="inModal.inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label="响应头">
<a-row>
<a-button size="small"
@click="inModal.inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inModal.inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before="名称"></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before="值">
<template slot="addonAfter">
<a-button size="small"
@click="inModal.inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
</template>
<!-- vmess kcp -->
<a-form v-if="inModal.inbound.stream.network === 'kcp'"
layout="inline">
<a-form-item label="伪装">
<a-select v-model="inModal.inbound.stream.kcp.type" style="width: 280px;">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="密码">
<a-input v-model.number="inModal.inbound.stream.kcp.seed"></a-input>
</a-form-item>
<a-form-item label="mtu">
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.mtu"></a-input>
</a-form-item>
<a-form-item label="tti (ms)">
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.tti"></a-input>
</a-form-item>
<a-form-item label="uplink capacity (MB/S)">
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.upCap"></a-input>
</a-form-item>
<a-form-item label="downlink capacity (MB/S)">
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.downCap"></a-input>
</a-form-item>
<a-form-item label="congestion">
<a-switch v-model="inModal.inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<a-form-item label="read buffer size (MB)">
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.readBuffer"></a-input>
</a-form-item>
<a-form-item label="write buffer size (MB)">
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.writeBuffer"></a-input>
</a-form-item>
</a-form>
<!-- vmess ws -->
<a-form v-if="inModal.inbound.stream.network === 'ws'"
layout="inline">
<a-form-item label="路径">
<a-input v-model.trim="inModal.inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label="请求头">
<a-row>
<a-button size="small"
@click="inModal.inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inModal.inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before="名称"></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before="值">
<template slot="addonAfter">
<a-button size="small"
@click="inModal.inbound.stream.ws.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
<!-- vmess http -->
<a-form v-if="inModal.inbound.stream.network === 'http'"
layout="inline">
<a-form-item label="路径">
<a-input v-model.trim="inModal.inbound.stream.http.path"></a-input>
</a-form-item>
<a-form-item label="host">
<a-row v-for="(host, index) in inModal.inbound.stream.http.host">
<a-input v-model.trim="inModal.inbound.stream.http.host[index]"></a-input>
</a-row>
</a-form-item>
</a-form>
<!-- vmess quic -->
<a-form v-if="inModal.inbound.stream.network === 'quic'"
layout="inline">
<a-form-item label="加密">
<a-select v-model="inModal.inbound.stream.quic.security" style="width: 165px;">
<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>
</a-select>
</a-form-item>
<a-form-item label="密码">
<a-input v-model.trim="inModal.inbound.stream.quic.key"></a-input>
</a-form-item>
<a-form-item label="伪装">
<a-select v-model="inModal.inbound.stream.quic.type" style="width: 280px;">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option>
</a-select>
</a-form-item>
</a-form>
</template>
<!-- dokodemo-door -->
<a-form v-if="inModal.inbound.protocol === Protocols.DOKODEMO"
layout="inline">
<a-form-item label="目标地址">
<a-input v-model.trim="inModal.inbound.settings.address"></a-input>
</a-form-item>
<a-form-item label="目标端口">
<a-input type="number" v-model.number="inModal.inbound.settings.port"></a-input>
</a-form-item>
<a-form-item label="网络">
<a-select v-model="inModal.inbound.settings.network" style="width: 100px;">
<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>
</a-select>
</a-form-item>
</a-form>
<!-- socks -->
<a-form v-if="inModal.inbound.protocol === Protocols.SOCKS"
layout="inline">
<a-form-item label="密码认证">
<a-switch :checked="inModal.inbound.settings.auth === 'password'"
@change="checked => inModal.inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<template v-if="inModal.inbound.settings.auth === 'password'">
<a-form-item label="用户名">
<a-input v-model.trim="inModal.inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label="密码">
<a-input v-model.trim="inModal.inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</template>
<a-form-item label="启用 udp">
<a-switch v-model="inModal.inbound.settings.udp"></a-switch>
</a-form-item>
<a-form-item v-if="inModal.inbound.settings.udp"
label="IP">
<a-input v-model.trim="inModal.inbound.settings.ip"></a-input>
</a-form-item>
</a-form>
<!-- http -->
<a-form v-if="inModal.inbound.protocol === Protocols.HTTP"
layout="inline">
<a-form-item label="用户名">
<a-input v-model.trim="inModal.inbound.settings.accounts[0].user"></a-input>
</a-form-item>
<a-form-item label="密码">
<a-input v-model.trim="inModal.inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</a-form>
<!-- tls settings -->
<template v-if="(inModal.inbound.protocol === Protocols.VMESS
|| inModal.inbound.protocol === Protocols.VLESS
|| inModal.inbound.protocol === Protocols.TROJAN
|| inModal.inbound.protocol === Protocols.SHADOWSOCKS)
&& ['tcp', 'ws', 'http', 'quic'].indexOf(inModal.inbound.stream.network) >= 0">
<!-- tls enable -->
<a-form layout="inline" v-if="inModal.inbound.protocol !== Protocols.TROJAN">
<a-form-item label="tls">
<a-switch
:checked="inModal.inbound.stream.security === 'tls'"
@change="checked => inModal.inbound.stream.security = checked ? 'tls' : 'none'">
</a-switch>
</a-form-item>
<a-form-item v-if="inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.stream.security === 'tls' && inModal.inbound.stream.network === 'tcp'" label="xtls">
<a-switch v-model="inModal.inbound.stream.is_xtls"></a-switch>
</a-form-item>
</a-form>
<!-- tls settings -->
<a-form v-if="inModal.inbound.stream.security === 'tls'"
layout="inline">
<a-form-item label="域名">
<a-input v-model.trim="inModal.inbound.stream.tls.server"></a-input>
</a-form-item>
{# <a-form-item label="允许不安全">#}
{# <a-switch v-model="inModal.inbound.stream.tls.allowInsecure"></a-switch>#}
{# </a-form-item>#}
<a-form-item label="证书">
<a-radio-group v-model="inModal.inbound.stream.tls.certs[0].useFile"
button-style="solid">
<a-radio-button :value="true">certificate file path</a-radio-button>
<a-radio-button :value="false">certificate file content</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inModal.inbound.stream.tls.certs[0].useFile">
<a-form-item label="公钥文件路径">
<a-input v-model.trim="inModal.inbound.stream.tls.certs[0].certFile"></a-input>
</a-form-item>
<a-form-item label="密钥文件路径">
<a-input v-model.trim="inModal.inbound.stream.tls.certs[0].keyFile"></a-input>
</a-form-item>
</template>
<template v-else>
<a-form-item label="公钥内容">
<a-input type="textarea" :rows="2"
v-model="inModal.inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label="密钥内容">
<a-input type="textarea" :rows="2"
v-model="inModal.inbound.stream.tls.certs[0].key"></a-input>
</a-form-item>
</template>
</a-form>
</template>
<!-- sniffing -->
<a-form layout="inline">
<a-form-item>
<span slot="label">
sniffing
<a-tooltip>
<template slot="title">
没有特殊需求保持默认即可
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inModal.inbound.sniffing.enabled"></a-switch>
</a-form-item>
</a-form>
</a-modal>
<script>
const inModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '确定',
confirm: null,
inbound: new Inbound(),
ok() {
ObjectUtil.execute(inModal.confirm);
},
show({ title='', okText='确定', inbound=null, confirm=()=>{} }) {
this.title = title;
this.okText = okText;
if (inbound) {
this.inbound = Inbound.fromJson(inbound.toJson());
} else {
this.inbound = new Inbound();
}
this.confirm = confirm;
this.visible = true;
},
close() {
inModal.visible = false;
inModal.closeLoading();
},
loading() {
inModal.confirmLoading = true;
},
closeLoading() {
inModal.confirmLoading = false;
}
};
const protocols = {
VMESS: Protocols.VMESS,
VLESS: Protocols.VLESS,
TROJAN: Protocols.TROJAN,
SHADOWSOCKS: Protocols.SHADOWSOCKS,
DOKODEMO: Protocols.DOKODEMO,
SOCKS: Protocols.SOCKS,
HTTP: Protocols.HTTP,
};
new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-modal',
data: {
inModal: inModal,
Protocols: protocols,
SSMethods: SSMethods,
},
methods: {
streamNetworkChange(oldValue) {
if (oldValue === 'kcp') {
this.inModal.inbound.stream.security = 'none';
}
},
protocolChange(value) {
this.inModal.inbound.settings = Inbound.Settings.getSettings(value);
if (value === Protocols.TROJAN) {
this.inModal.inbound.stream.security = 'tls';
}
}
}
});
</script>
{{end}}

286
web/html/x-ui/inbounds.html Normal file
View File

@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
.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="true" 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;">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
</div>
<a-row>
<a-input v-model="searchKey" placeholder="search" autofocus></a-input>
</a-row>
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
upload / download
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
total traffic
<a-popconfirm title="Are you sure you want to reset all traffic to 0? It\'s unrecoverable"
@confirm="resetAllTraffic()"
ok-text="confirm" cancel-text="cancel">
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
</a-popconfirm>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
number of accounts
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
</a-row>
</a-card>
</transition>
<transition name="list" appear>
<a-card hoverable>
<a-table :columns="columns" :row-key="inbound => inbound.id"
:data-source="dbInbounds"
:loading="spinning" :scroll="{ x: 1500 }"
:pagination="false"
@change="() => getDBInbounds()">
<template slot="protocol" slot-scope="text, inbound">
<a-tag color="blue">[[ inbound.protocol ]]</a-tag>
</template>
<template slot="settings" slot-scope="text, inbound">
<a-button type="link">查看</a-button>
</template>
<template slot="streamSettings" slot-scope="text, inbound">
<a-button type="link">查看</a-button>
</template>
<template slot="enable" slot-scope="text, inbound">
<a-tag v-if="inbound.enable" color="green">启用</a-tag>
<a-tag v-else color="red">禁用</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, inbound">
<span v-if="inbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(inbound.expiryTime) ]]</span>
<span v-else>无限期</span>
</template>
<template slot="action" slot-scope="text, inbound">
<a-button type="primary" icon="qrcode"></a-button>
<a-button type="primary" icon="edit"></a-button>
<a-button type="danger" icon="delete" @click="delInbound(inbound)"></a-button>
</template>
<template slot="expandedRowRender" slot-scope="inbound" style="margin: 0">
[[ inbound.id ]]
</template>
</a-table>
</a-card>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
<script>
const columns = [{
title: "id",
align: 'center',
dataIndex: "id",
width: 60,
}, {
title: "protocol",
align: 'center',
width: 60,
scopedSlots: { customRender: 'protocol' },
}, {
title: "port",
align: 'center',
dataIndex: "port",
width: 60,
}, {
title: "settings",
align: 'center',
width: 60,
scopedSlots: { customRender: 'settings' },
}, {
title: "streamSettings",
align: 'center',
width: 60,
scopedSlots: { customRender: 'streamSettings' },
}, {
title: "enable",
align: 'center',
width: 60,
scopedSlots: { customRender: 'enable' },
}, {
title: "expiryTime",
align: 'center',
width: 60,
scopedSlots: { customRender: 'expiryTime' },
}, {
title: "action",
align: 'center',
width: 60,
scopedSlots: { customRender: 'action' },
}];
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
ip: location.hostname,
spinning: false,
dbInbounds: [],
searchKey: '',
},
methods: {
loading(spinning=true) {
this.spinning = spinning;
},
empDefault(o, defaultValue='') {return ObjectUtil.isEmpty(o) ? defaultValue : o},
async getDBInbounds() {
this.loading();
const msg = await HttpUtil.post('/xui/inbounds');
this.loading(false);
if (!msg.success) {
return;
}
this.setInbounds(msg.obj);
},
setInbounds(dbInbounds) {
this.dbInbounds.splice(0);
for (const inbound of dbInbounds) {
this.dbInbounds.push(new DBInbound(inbound));
}
},
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)) {
this.searchedInbounds.push(inbound);
}
});
}
},
openAddInbound() {
inModal.show({
title: 'add account',
okText: 'add',
confirm: async () => {
inModal.loading();
await this.addInbound(inModal.inbound);
inModal.closeLoading();
}
});
},
openEditInbound(inbound) {
inModal.show({
title: 'update account',
okText: 'update',
inbound: inbound,
confirm: async () => {
inModal.loading();
inModal.inbound.id = inbound.id;
await this.updateInbound(inModal.inbound);
inModal.closeLoading();
}
});
},
async addInbound(inbound) {
let data = {
port: inbound.port,
listen: inbound.listen,
protocol: inbound.protocol,
settings: inbound.settings.toString(false),
stream_settings: inbound.stream.toString(false),
sniffing: [Protocols.VMESS, Protocols.VLESS, Protocols.SHADOWSOCKS].indexOf(inbound.protocol) >= 0 ? inbound.sniffing.toString(false) : '{}',
remark: inbound.remark,
};
await this.submit('/xui/inbound/add', data, inModal);
},
async updateInbound(inbound) {
const data = {
port: inbound.port,
listen: inbound.listen,
protocol: inbound.protocol,
settings: inbound.settings.toString(false),
stream_settings: inbound.stream.toString(false),
sniffing: [Protocols.VMESS, Protocols.VLESS, Protocols.SHADOWSOCKS].indexOf(inbound.protocol) >= 0 ? inbound.sniffing.toString(false) : '{}',
remark: inbound.remark,
enable: inbound.enable,
};
await this.submit(`/xui/inbound/update/${inbound.id}`, data, inModal);
},
delInbound(inbound) {
this.$confirm({
title: 'delete account',
content: 'Cannot be restored after deletion, confirm deletion?',
okText: 'delete',
cancelText: 'cancel',
onOk: () => this.submit('/xui/inbound/del/' + inbound.id),
});
},
resetTraffic(inbound) {
this.submit(`/xui/reset_traffic/${inbound.id}`);
},
resetAllTraffic() {
this.submit('/xui/reset_all_traffic');
},
setEnable(inbound, enable) {
let data = {enable: enable};
this.submit(`/xui/inbound/update/${inbound.id}`, data);
},
async submit(url, data, modal) {
const msg = await HttpUtil.post(url, data);
if (msg.success) {
this.getDBInbounds();
if (modal != null) {
modal.close();
}
}
return msg;
},
},
watch: {
searchKey(value) {
this.searchInbounds(value);
}
},
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"}}
</body>
</html>

View File

@@ -68,7 +68,7 @@
<a-card hoverable>
xray 状态:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.stat === State.Error">
<a-tooltip v-if="status.xray.state === State.Error">
<template slot="title">
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
</template>
@@ -175,7 +175,6 @@
</template>
</a-modal>
</a-layout>
</body>
{{template "js" .}}
<script>
@@ -330,4 +329,5 @@
});
</script>
</body>
</html>