first commit

This commit is contained in:
sprov
2021-05-18 12:59:22 +08:00
parent bc6a518c9a
commit 56ed8f355c
62 changed files with 63166 additions and 0 deletions

17
web/html/common/head.html Normal file
View File

@@ -0,0 +1,17 @@
{{define "head"}}
<head>
<meta charset="UTF-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<style>
[v-cloak] {
display: none;
}
</style>
<title>{{.title}}</title>
</head>
{{end}}

20
web/html/common/js.html Normal file
View File

@@ -0,0 +1,20 @@
{{define "js"}}
<script src="{{ .base_path }}assets/vue@2.6.12/vue.min.js"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.js"></script>
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/axios/axios.min.js"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
<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>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
</script>
{{end}}

View File

@@ -0,0 +1,67 @@
{{define "promptModel"}}
<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="取消">
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"
:autosize="{minRows: 10, maxRows: 20}"
@keydown.enter.native="promptModal.keyEnter"
@keydown.ctrl.83="promptModal.ctrlS"></a-input>
</a-modal>
<script>
const promptModal = {
title: '',
type: '',
value: '',
okText: '确定',
visible: false,
keyEnter(e) {
if (this.type !== 'textarea') {
e.preventDefault();
this.ok();
}
},
ctrlS(e) {
if (this.type === 'textarea') {
e.preventDefault();
promptModal.confirm(promptModal.value);
}
},
ok() {
promptModal.close();
promptModal.confirm(promptModal.value);
},
confirm() {},
open({
title='',
type='text',
value='',
okText='确定',
confirm=() => {},
}) {
this.title = title;
this.type = type;
this.value = value;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
promptModalApp.$nextTick(() => {
document.querySelector('#prompt-modal-input').focus();
});
},
close() {
this.visible = false;
}
};
const promptModalApp = new Vue({
el: '#prompt-modal',
data: {
promptModal: promptModal,
},
});
</script>
{{end}}

90
web/html/login/login.html Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
#app {
padding-top: 100px;
}
h1 {
text-align: center;
color: #fff;
margin: 20px 0 50px 0;
}
.ant-btn, .ant-input {
height: 50px;
border-radius: 30px;
}
.ant-input-affix-wrapper .ant-input-prefix {
left: 23px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 50px;
}
</style>
<body>
<a-layout id="app" v-cloak>
<transition name="list" appear>
<a-layout-content>
<a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1>{{ .title }}</h1>
</a-col>
</a-row>
<a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<a-form>
<a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
</a-input>
</a-form-item>
<a-form-item>
<a-input type="password" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
</a-input>
</a-form-item>
<a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout>
{{template "js" .}}
<script>
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = background;
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
loading: false,
user: new User(),
},
methods: {
async login() {
this.loading = true;
const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) {
location.href = basePath + 'xui/';
}
}
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
{{define "menuItems"}}
<a-menu-item key="{{ .base_path }}xui/">
<a-icon type="dashboard"></a-icon>
<span>系统状态</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/accounts">
<a-icon type="user"></a-icon>
<span>账号列表</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/setting">
<a-icon type="setting"></a-icon>
<span>面板设置</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/clients">
<a-icon type="laptop"></a-icon>
<span>客户端</span>
</a-menu-item>
<a-sub-menu>
<template slot="title">
<a-icon type="link"></a-icon>
<span>其他</span>
</template>
<a-menu-item key="https://github.com/sprov065/x-ui/">
<a-icon type="github"></a-icon>
<span>Github</span>
</a-menu-item>
</a-sub-menu>
<a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon>
<span>退出登录</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}}

333
web/html/x-ui/index.html Normal file
View File

@@ -0,0 +1,333 @@
<!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="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>
内存: [[ 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>
硬盘: [[ 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>
xray 状态:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.stat === 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">切换版本</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
运行时间:
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
<template slot="title">
系统自启动以来的运行时间
</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>
系统负载: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable>
tcp / udp 连接数: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
所有网卡的总连接数
</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">
所有网卡的总上传速度
</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">
所有网卡的总下载速度
</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">
系统启动以来所有网卡的总上传流量
</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">
系统启动以来所有网卡的总下载流量
</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="切换版本"
:closable="true" @ok="() => versionModal.visible = false"
ok-text="确定" cancel-text="取消">
<h2>点击你想切换的版本</h2>
<h2>请谨慎选择,旧版本可能配置不兼容</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>
</body>
{{template "js" .}}
<script>
const State = {
Running: "running",
Stop: "stop",
Error: "error",
}
Object.freeze(State);
class CurTotal {
current = 0
total = 0
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 {
cpu = new CurTotal(0, 0);
disk = new CurTotal(0, 0);
loads = [0, 0, 0];
mem = new CurTotal(0, 0);
netIO = {up: 0, down: 0};
netTraffic = {sent: 0, recv: 0};
swap = new CurTotal(0, 0);
tcpCount = 0;
udpCount = 0;
uptime = 0;
xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
constructor(data) {
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: {
status: new Status(),
versionModal,
spinning: false,
loadingTip: '加载中',
},
methods: {
loading(spinning, tip = '加载中') {
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: '切换 xray 版本',
content: '是否切换 xray 版本至' + ` ${version}?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
versionModal.hide();
this.loading(true, '安装中,请不要刷新此页面');
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>
</html>