mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-12-17 20:54:40 +03:00
3x-ui
This commit is contained in:
73
web/html/xui/common_sider.html
Normal file
73
web/html/xui/common_sider.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{{define "menuItems"}}
|
||||
<a-menu-item key="{{ .base_path }}xui/">
|
||||
<a-icon type="dashboard"></a-icon>
|
||||
<span>{{ i18n "menu.dashboard"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/inbounds">
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>{{ i18n "menu.inbounds"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/setting">
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span>{{ i18n "menu.setting"}}</span>
|
||||
</a-menu-item>
|
||||
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
||||
<!-- <a-icon type="laptop"></a-icon>-->
|
||||
<!-- <span>client</span>-->
|
||||
<!--</a-menu-item>-->
|
||||
<a-sub-menu>
|
||||
<template slot="title">
|
||||
<a-icon type="link"></a-icon>
|
||||
<span>others</span>
|
||||
</template>
|
||||
<a-menu-item key="https://github.com/mhsanaei/3x-ui/">
|
||||
<a-icon type="github"></a-icon>
|
||||
<span>Github</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="https://t.me/xxxuiforever">
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
<span>Telegram Group</span>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item key="{{ .base_path }}logout">
|
||||
<a-icon type="logout"></a-icon>
|
||||
<span>{{ i18n "menu.logout"}}</span>
|
||||
</a-menu-item>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{define "commonSider"}}
|
||||
<a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||
@close="siderDrawer.close()"
|
||||
:visible="siderDrawer.visible" :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="light" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
<script>
|
||||
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
show() {
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
},
|
||||
change() {
|
||||
this.visible = !this.visible;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
94
web/html/xui/component/inbound_info.html
Normal file
94
web/html/xui/component/inbound_info.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{{define "inboundInfoStream"}}
|
||||
<p>{{ i18n "transmission" }}: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
|
||||
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
|
||||
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
|
||||
<p v-else>{{ i18n "host" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
|
||||
|
||||
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
|
||||
<p v-else>{{ i18n "path" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isQuic">
|
||||
<p>quic {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
|
||||
<p>quic {{ i18n "password" }}: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
|
||||
<p>quic {{ i18n "camouflage" }}: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isKcp">
|
||||
<p>kcp {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
|
||||
<p>kcp {{ i18n "password" }}: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.tls || inbound.xtls">
|
||||
<p v-if="inbound.tls">tls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
|
||||
<p v-if="inbound.xtls">xtls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>tls: <a-tag color="red">{{ i18n "closure" }}</a-tag></p>
|
||||
</template>
|
||||
<p v-if="inbound.tls">
|
||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</p>
|
||||
<p v-if="inbound.xtls">
|
||||
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{define "component/inboundInfoComponent"}}
|
||||
<div>
|
||||
<p>{{ i18n "protocol"}}: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
|
||||
<p>{{ i18n "pages.inbounds.address"}}: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
|
||||
<p>{{ i18n "pages.inbounds.port"}}: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
|
||||
|
||||
<template v-if="dbInbound.isVMess" v-for="(vmess, index) in inbound.settings.vmesses">
|
||||
<p>uuid: <a-tag color="green">[[ vmess.id ]]</a-tag></p>
|
||||
<p>alterId: <a-tag color="green">[[ vmess.alterId ]]</a-tag></p>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isVLess" v-for="(vless, index) in inbound.settings.vlesses">
|
||||
<p>uuid: <a-tag color="green">[[ vless.id ]]</a-tag></p>
|
||||
<p v-if="inbound.isXTls">flow: <a-tag color="green">[[ vless.flow ]]</a-tag></p>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isTrojan">
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isSS">
|
||||
<p>{{ i18n "encryption"}}: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isSocks">
|
||||
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isHTTP">
|
||||
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
{{template "inboundInfoStream"}}
|
||||
</template>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "component/inboundInfo"}}
|
||||
<script>
|
||||
Vue.component('inbound-info', {
|
||||
delimiters: ['[[', ']]'],
|
||||
props: ["dbInbound", "inbound"],
|
||||
template: `{{template "component/inboundInfoComponent"}}`,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
32
web/html/xui/component/setting.html
Normal file
32
web/html/xui/component/setting.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{{define "component/settingListItem"}}
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta :title="title" :description="desc"/>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template v-if="type === 'text'">
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'textarea'">
|
||||
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
|
||||
</template>
|
||||
<template v-else-if="type === 'switch'">
|
||||
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
{{end}}
|
||||
|
||||
{{define "component/setting"}}
|
||||
<script>
|
||||
Vue.component('setting-list-item', {
|
||||
props: ["type", "title", "desc", "value"],
|
||||
template: `{{template "component/settingListItem"}}`,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
106
web/html/xui/form/inbound.html
Normal file
106
web/html/xui/form/inbound.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" style="width: 160px;">
|
||||
<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">
|
||||
{{ i18n "monitor" }}
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="inbound.listen"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input type="number" v-model.number="inbound.port"></a-input>
|
||||
</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="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
<template v-if="inbound.protocol === Protocols.VMESS">
|
||||
{{template "form/vmess"}}
|
||||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="inbound.protocol === Protocols.VLESS">
|
||||
{{template "form/vless"}}
|
||||
</template>
|
||||
|
||||
<!-- trojan settings -->
|
||||
<template v-if="inbound.protocol === Protocols.TROJAN">
|
||||
{{template "form/trojan"}}
|
||||
</template>
|
||||
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
{{template "form/shadowsocks"}}
|
||||
</template>
|
||||
|
||||
<!-- dokodemo-door -->
|
||||
<template v-if="inbound.protocol === Protocols.DOKODEMO">
|
||||
{{template "form/dokodemo"}}
|
||||
</template>
|
||||
|
||||
<!-- socks -->
|
||||
<template v-if="inbound.protocol === Protocols.SOCKS">
|
||||
{{template "form/socks"}}
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
<template v-if="inbound.protocol === Protocols.HTTP">
|
||||
{{template "form/http"}}
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="inbound.canEnableStream()">
|
||||
{{template "form/streamSettings"}}
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="inbound.canEnableTls()">
|
||||
{{template "form/tlsSettings"}}
|
||||
</template>
|
||||
|
||||
<!-- sniffing -->
|
||||
<template v-if="inbound.canSniffing()">
|
||||
{{template "form/sniffing"}}
|
||||
</template>
|
||||
{{end}}
|
||||
17
web/html/xui/form/protocol/dokodemo.html
Normal file
17
web/html/xui/form/protocol/dokodemo.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{{define "form/dokodemo"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
||||
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||
<a-select v-model="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>
|
||||
{{end}}
|
||||
10
web/html/xui/form/protocol/http.html
Normal file
10
web/html/xui/form/protocol/http.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{{define "form/http"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "username"}}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
19
web/html/xui/form/protocol/shadowsocks.html
Normal file
19
web/html/xui/form/protocol/shadowsocks.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{{define "form/shadowsocks"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="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='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="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>
|
||||
{{end}}
|
||||
24
web/html/xui/form/protocol/socks.html
Normal file
24
web/html/xui/form/protocol/socks.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{{define "form/socks"}}
|
||||
<a-form layout="inline">
|
||||
<!-- <a-form-item label="密码认证">-->
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.settings.udp"
|
||||
label="IP">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
141
web/html/xui/form/protocol/trojan.html
Normal file
141
web/html/xui/form/protocol/trojan.html
Normal file
@@ -0,0 +1,141 @@
|
||||
{{define "form/trojan"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
|
||||
:key="`trojan-${index}`">
|
||||
|
||||
|
||||
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
</template>
|
||||
<!--Renew Svg Icon-->
|
||||
<svg
|
||||
@click="getNewEmail(trojan)"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="trojan.email"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="password">
|
||||
<a-input v-model.trim="trojan.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.trojans[index].flow" style="width: 150px">
|
||||
<a-select-option value="" selected>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="trojan._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="trojan._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tooltip v-if="trojan._totalGB > 0">
|
||||
<template slot="title">
|
||||
reset traffic
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
|
||||
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
|
||||
<a-tag>
|
||||
<svg
|
||||
@click="addClient(inbound.protocol,trojan, inbound.settings.trojans)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
<a-tag v-show="inbound.settings.trojans.length > 1">
|
||||
<svg
|
||||
v-show="inbound.settings.trojans.length > 1"
|
||||
@click="removeClient(index, inbound.settings.trojans)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</a-form>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addTrojanFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<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-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
{{end}}
|
||||
155
web/html/xui/form/protocol/vless.html
Normal file
155
web/html/xui/form/protocol/vless.html
Normal file
@@ -0,0 +1,155 @@
|
||||
{{define "form/vless"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
|
||||
:key="`vless-${index}`">
|
||||
|
||||
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
</template>
|
||||
<!--Renew Svg Icon-->
|
||||
<svg
|
||||
@click="getNewEmail(vless)"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="vless.email"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vless.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
||||
<a-select-option value="" selected>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" layout="inline">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
||||
<a-select-option value="" selected>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 v-if="inbound.tls" label="utls" layout="inline">
|
||||
<a-select v-model="inbound.settings.vlesses[index].fingerprint" label="utls" style="width: 150px">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :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="vless._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tooltip v-if="vless._totalGB > 0">
|
||||
<template slot="title">
|
||||
reset traffic
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
|
||||
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
|
||||
<a-tag>
|
||||
<svg
|
||||
|
||||
@click="addClient(inbound.protocol,vless, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
<a-tag v-show="inbound.settings.vlesses.length > 1">
|
||||
<svg
|
||||
@click="removeClient(index, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</a-form>
|
||||
|
||||
|
||||
</a-form>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<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-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
{{end}}
|
||||
119
web/html/xui/form/protocol/vmess.html
Normal file
119
web/html/xui/form/protocol/vmess.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{{define "form/vmess"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
|
||||
:key="`vmess-${index}`">
|
||||
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
</template>
|
||||
<!--Renew Svg Icon-->
|
||||
<svg
|
||||
@click="getNewEmail(vmess)"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="vmess.email"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vmess.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||
<a-input type="number" v-model.number="vmess.alterId"></a-input>
|
||||
</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="vmess._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tooltip v-if="vmess._totalGB > 0">
|
||||
<template slot="title">
|
||||
reset traffic
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
|
||||
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
|
||||
<a-tag>
|
||||
<!--Add Svg Icon-->
|
||||
<svg
|
||||
|
||||
@click="addClient(inbound.protocol,vmess, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
<a-tag v-show="inbound.settings.vmesses.length > 1">
|
||||
|
||||
<!--Remove Svg Icon-->
|
||||
<svg
|
||||
@click="removeClient(index, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</a-form>
|
||||
|
||||
|
||||
</a-collapse-panel>
|
||||
|
||||
</a-collapse>
|
||||
|
||||
|
||||
</a-form>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
||||
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
{{end}}
|
||||
16
web/html/xui/form/sniffing.html
Normal file
16
web/html/xui/form/sniffing.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{{define "form/sniffing"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
sniffing
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
7
web/html/xui/form/stream/stream_grpc.html
Normal file
7
web/html/xui/form/stream/stream_grpc.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{{define "form/streamGRPC"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="serviceName">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
12
web/html/xui/form/stream/stream_http.html
Normal file
12
web/html/xui/form/stream/stream_http.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{define "form/streamHTTP"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="host">
|
||||
<a-row v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
38
web/html/xui/form/stream/stream_kcp.html
Normal file
38
web/html/xui/form/stream/stream_kcp.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{{define "form/streamKCP"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
|
||||
<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>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.number="inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="mtu">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="tti (ms)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uplink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="downlink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="congestion">
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="read buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="write buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
24
web/html/xui/form/stream/stream_quic.html
Normal file
24
web/html/xui/form/stream/stream_quic.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{{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;">
|
||||
<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='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
|
||||
<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>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
45
web/html/xui/form/stream/stream_settings.html
Normal file
45
web/html/xui/form/stream/stream_settings.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="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-option value="grpc">grpc</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp -->
|
||||
<template v-if="inbound.stream.network === 'tcp'">
|
||||
{{template "form/streamTCP"}}
|
||||
</template>
|
||||
|
||||
<!-- kcp -->
|
||||
<template v-if="inbound.stream.network === 'kcp'">
|
||||
{{template "form/streamKCP"}}
|
||||
</template>
|
||||
|
||||
<!-- ws -->
|
||||
<template v-if="inbound.stream.network === 'ws'">
|
||||
{{template "form/streamWS"}}
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
<template v-if="inbound.stream.network === 'http'">
|
||||
{{template "form/streamHTTP"}}
|
||||
</template>
|
||||
|
||||
<!-- quic -->
|
||||
<template v-if="inbound.stream.network === 'quic'">
|
||||
{{template "form/streamQUIC"}}
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="inbound.stream.network === 'grpc'">
|
||||
{{template "form/streamGRPC"}}
|
||||
</template>
|
||||
{{end}}
|
||||
86
web/html/xui/form/stream/stream_tcp.html
Normal file
86
web/html/xui/form/stream/stream_tcp.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{{define "form/streamTCP"}}
|
||||
<!-- tcp type -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="http camouflage">
|
||||
<a-switch
|
||||
:checked="inbound.stream.tcp.type === 'http'"
|
||||
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp request -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestPath" }}'>
|
||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp response -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
33
web/html/xui/form/stream/stream_ws.html
Normal file
33
web/html/xui/form/stream/stream_ws.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
60
web/html/xui/form/tls_settings.html
Normal file
60
web/html/xui/form/tls_settings.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form layout="inline" v-if="inbound.canSetTls()">
|
||||
<a-form-item label="tls">
|
||||
<a-switch v-model="inbound.tls">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.canEnableXTls()" label="xtls">
|
||||
<a-switch v-model="inbound.xtls"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inbound.tls || inbound.xtls"layout="inline">
|
||||
<a-form-item label="minVersion">
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px">
|
||||
<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">
|
||||
<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="cipherSuites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
|
||||
<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>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.stream.tls.certs[0].useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="2" v-model="inbound.stream.tls.certs[0].cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="2" v-model="inbound.stream.tls.certs[0].key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
61
web/html/xui/inbound_info_modal.html
Normal file
61
web/html/xui/inbound_info_modal.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{{define "inboundInfoModal"}}
|
||||
{{template "component/inboundInfo"}}
|
||||
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' @ok="infoModal.ok"
|
||||
:closable="true" :mask-closable="true"
|
||||
ok-text='{{ i18n "pages.inbounds.copyLink"}}' cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
|
||||
<inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
clipboard: null,
|
||||
okBtnPros: {
|
||||
attrs: {
|
||||
id: "inbound-info-modal-ok-btn",
|
||||
style: "",
|
||||
},
|
||||
},
|
||||
show(dbInbound) {
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.visible = true;
|
||||
|
||||
if (dbInbound.hasLink()) {
|
||||
this.okBtnPros.attrs.style = "";
|
||||
} else {
|
||||
this.okBtnPros.attrs.style = "display: none";
|
||||
}
|
||||
|
||||
if (this.clipboard == null) {
|
||||
infoModalApp.$nextTick(() => {
|
||||
this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
|
||||
text: () => this.dbInbound.genLink(),
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}'));
|
||||
});
|
||||
}
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const infoModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-info-modal',
|
||||
data: {
|
||||
infoModal,
|
||||
get dbInbound() {
|
||||
return this.infoModal.dbInbound;
|
||||
},
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
178
web/html/xui/inbound_modal.html
Normal file
178
web/html/xui/inbound_modal.html
Normal file
@@ -0,0 +1,178 @@
|
||||
{{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" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const inModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
ok() {
|
||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
if (inbound) {
|
||||
this.inbound = Inbound.fromJson(inbound.toJson());
|
||||
} else {
|
||||
this.inbound = new Inbound();
|
||||
}
|
||||
if (dbInbound) {
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
} else {
|
||||
this.dbInbound = new DBInbound();
|
||||
}
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
this.isEdit = isEdit;
|
||||
},
|
||||
close() {
|
||||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
get inbound() {
|
||||
return inModal.inbound;
|
||||
},
|
||||
get dbInbound() {
|
||||
return inModal.dbInbound;
|
||||
},
|
||||
get isEdit() {
|
||||
return inModal.isEdit;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange(oldValue) {
|
||||
if (oldValue === 'kcp') {
|
||||
this.inModal.inbound.tls = false;
|
||||
}
|
||||
},
|
||||
addClient(protocol, clients) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
removeClient(index, clients) {
|
||||
clients.splice(index, 1);
|
||||
},
|
||||
|
||||
async resetClientTraffic(client,event) {
|
||||
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
clientStats = this.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(index) {
|
||||
return this.inbound.isExpiry(index)
|
||||
},
|
||||
getUpStats(email) {
|
||||
clientStats = this.inbound.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(email) {
|
||||
clientStats = this.inbound.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']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isClientEnable(email) {
|
||||
clientStats = this.inbound.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']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getHeaderText(email) {
|
||||
if(email == "")
|
||||
return "Add Client"
|
||||
|
||||
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
|
||||
},
|
||||
|
||||
getHeaderStyle(email) {
|
||||
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
|
||||
},
|
||||
|
||||
getNewEmail(client) {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
var string = '';
|
||||
var len = 6 + Math.floor(Math.random() * 5)
|
||||
for(var ii=0; ii<len; ii++){
|
||||
string += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
client.email = string + "@gmail.com"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
457
web/html/xui/inbounds.html
Normal file
457
web/html/xui/inbounds.html
Normal file
@@ -0,0 +1,457 @@
|
||||
<!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="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>-->
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="dbInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1500 }"
|
||||
:pagination="false"
|
||||
style="margin-top: 20px"
|
||||
@change="() => getDBInbounds()">
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-icon type="edit" @click="openEditInbound(dbInbound)"></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.hasLink()" 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="settings" slot-scope="text, dbInbound">
|
||||
<a-button type="link" @click="showInfo(dbInbound)">{{ i18n "check" }}</a-button>
|
||||
</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) || (record.protocol === Protocols.TROJAN)"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
>
|
||||
{{template "form/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: 40,
|
||||
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: 100,
|
||||
dataIndex: "remark",
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.port" }}',
|
||||
align: 'center',
|
||||
dataIndex: "port",
|
||||
width: 60,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.details" }}',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'settings' },
|
||||
}, {
|
||||
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: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 100, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: '{{ i18n "pages.inbounds.uid" }}', width: 150, dataIndex: "id" },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
spinning: false,
|
||||
inbounds: [],
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
},
|
||||
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);
|
||||
for (const inbound of dbInbounds) {
|
||||
const dbInbound = new DBInbound(inbound);
|
||||
this.inbounds.push(dbInbound.toInbound());
|
||||
this.dbInbounds.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)) {
|
||||
this.searchedInbounds.push(inbound);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
clickAction(action, dbInbound) {
|
||||
switch (action.key) {
|
||||
case "qrcode":
|
||||
this.showQrcode(dbInbound);
|
||||
break;
|
||||
case "edit":
|
||||
this.openEditInbound(dbInbound);
|
||||
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) {
|
||||
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) {
|
||||
const link = dbInbound.genLink();
|
||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
|
||||
},
|
||||
showInfo(dbInbound) {
|
||||
infoModal.show(dbInbound);
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
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']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
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"}}
|
||||
{{template "inboundInfoModal"}}
|
||||
</body>
|
||||
</html>
|
||||
22
web/html/xui/inbounds_client_row.html
Normal file
22
web/html/xui/inbounds_client_row.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{{define "form/client_row"}}
|
||||
<template slot="client" slot-scope="text, client">
|
||||
[[ client.email ]]
|
||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red"> expired</a-tag>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-tag v-if="client._totalGB === 0" color="blue">{{ i18n "used" }}: [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]]</a-tag>
|
||||
<a-tag v-if="client._totalGB > 0 && !isTrafficExhausted(record, client.email)" color="green">{{ i18n "used" }}: [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / [[client._totalGB]]GB</a-tag>
|
||||
<a-tag v-if="client._totalGB > 0 && isTrafficExhausted(record, client.email)" color="red">{{ i18n "used" }}: [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / [[client._totalGB]]GB</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client._expiryTime > 0">
|
||||
<a-tag v-if="isExpiry(record, index)" color="red">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else color="blue">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</template>
|
||||
{{end}}
|
||||
334
web/html/xui/index.html
Normal file
334
web/html/xui/index.html
Normal file
@@ -0,0 +1,334 @@
|
||||
<!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="200" :tip="loadingTip"/>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<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"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div>CPU</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<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.swap.color"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
<div>
|
||||
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-row>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<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">
|
||||
<template slot="title">
|
||||
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.operationHoursDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
[[ sizeFormat(status.netIO.up) ]] / S
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
[[ sizeFormat(status.netIO.down) ]] / S
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
[[ sizeFormat(status.netTraffic.sent) ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalSent" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-download"></a-icon>
|
||||
[[ sizeFormat(status.netTraffic.recv) ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalReceive" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</transition>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||
:closable="true" @ok="() => versionModal.visible = false"
|
||||
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
||||
<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'"
|
||||
style="margin: 10px" @click="switchV2rayVersion(version)">
|
||||
[[ version ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script>
|
||||
|
||||
const State = {
|
||||
Running: "running",
|
||||
Stop: "stop",
|
||||
Error: "error",
|
||||
}
|
||||
Object.freeze(State);
|
||||
|
||||
class CurTotal {
|
||||
|
||||
constructor(current, total) {
|
||||
this.current = current;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
get percent() {
|
||||
if (this.total === 0) {
|
||||
return 0;
|
||||
}
|
||||
return toFixed(this.current / this.total * 100, 2);
|
||||
}
|
||||
|
||||
get color() {
|
||||
const percent = this.percent;
|
||||
if (percent < 80) {
|
||||
return '#67C23A';
|
||||
} else if (percent < 90) {
|
||||
return '#E6A23C';
|
||||
} else {
|
||||
return '#F56C6C';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Status {
|
||||
constructor(data) {
|
||||
this.cpu = new CurTotal(0, 0);
|
||||
this.disk = new CurTotal(0, 0);
|
||||
this.loads = [0, 0, 0];
|
||||
this.mem = new CurTotal(0, 0);
|
||||
this.netIO = {up: 0, down: 0};
|
||||
this.netTraffic = {sent: 0, recv: 0};
|
||||
this.swap = new CurTotal(0, 0);
|
||||
this.tcpCount = 0;
|
||||
this.udpCount = 0;
|
||||
this.uptime = 0;
|
||||
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
this.cpu = new CurTotal(data.cpu, 100);
|
||||
this.disk = new CurTotal(data.disk.current, data.disk.total);
|
||||
this.loads = data.loads.map(load => toFixed(load, 2));
|
||||
this.mem = new CurTotal(data.mem.current, data.mem.total);
|
||||
this.netIO = data.netIO;
|
||||
this.netTraffic = data.netTraffic;
|
||||
this.swap = new CurTotal(data.swap.current, data.swap.total);
|
||||
this.tcpCount = data.tcpCount;
|
||||
this.udpCount = data.udpCount;
|
||||
this.uptime = data.uptime;
|
||||
this.xray = data.xray;
|
||||
switch (this.xray.state) {
|
||||
case State.Running:
|
||||
this.xray.color = "green";
|
||||
break;
|
||||
case State.Stop:
|
||||
this.xray.color = "orange";
|
||||
break;
|
||||
case State.Error:
|
||||
this.xray.color = "red";
|
||||
break;
|
||||
default:
|
||||
this.xray.color = "gray";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const versionModal = {
|
||||
visible: false,
|
||||
versions: [],
|
||||
show(versions) {
|
||||
this.visible = true;
|
||||
this.versions = versions;
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
status: new Status(),
|
||||
versionModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
},
|
||||
methods: {
|
||||
loading(spinning, tip = '{{ i18n "loading"}}') {
|
||||
this.spinning = spinning;
|
||||
this.loadingTip = tip;
|
||||
},
|
||||
async getStatus() {
|
||||
const msg = await HttpUtil.post('/server/status');
|
||||
if (msg.success) {
|
||||
this.setStatus(msg.obj);
|
||||
}
|
||||
},
|
||||
setStatus(data) {
|
||||
this.status = new Status(data);
|
||||
},
|
||||
async openSelectV2rayVersion() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/getXrayVersion');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
versionModal.show(msg.obj);
|
||||
},
|
||||
switchV2rayVersion(version) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||
okText: '{{ i18n "confirm"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
versionModal.hide();
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
|
||||
await HttpUtil.post(`/server/installXray/${version}`);
|
||||
this.loading(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
while (true) {
|
||||
try {
|
||||
await this.getStatus();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await PromiseUtil.sleep(2000);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
196
web/html/xui/setting.html
Normal file
196
web/html/xui/setting.html
Normal file
@@ -0,0 +1,196 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-tabs-top-bar {
|
||||
background: white;
|
||||
}
|
||||
</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">
|
||||
<a-space direction="vertical">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
|
||||
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language"/>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<temlate>
|
||||
<a-select
|
||||
ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</temlate>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
|
||||
<a-form style="background: white; padding: 20px">
|
||||
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
|
||||
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
|
||||
<a-input type="password" v-model="user.oldPassword"
|
||||
style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
|
||||
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
|
||||
<a-input type="password" v-model="user.newPassword"
|
||||
style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<!-- <a-button type="primary" @click="updateUser">update</a-button>-->
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/setting"}}
|
||||
<script>
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
spinning: false,
|
||||
oldAllSetting: new AllSetting(),
|
||||
allSetting: new AllSetting(),
|
||||
saveBtnDisable: true,
|
||||
user: {},
|
||||
lang : getLang()
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/all");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.oldAllSetting = new AllSetting(msg.obj);
|
||||
this.allSetting = new AllSetting(msg.obj);
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async updateAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await this.getAllSetting();
|
||||
}
|
||||
},
|
||||
async updateUser() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.user = {};
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
await new Promise(resolve => {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.setting.restartPanel" }}',
|
||||
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
|
||||
okText: '{{ i18n "sure" }}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => resolve(),
|
||||
});
|
||||
});
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.loading(true);
|
||||
await PromiseUtil.sleep(5000);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getAllSetting();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user