UI Improvements (#2228)

* UI Improvements

Better Table
Update QR Code Modal
Better Info Modal
Compression HTML files
Better Dropdown Menu
Better Calendar
and more ..
Remove files
Minor Fixes
This commit is contained in:
Tara Rostami 2024-04-20 22:15:36 +03:30 committed by GitHub
parent 3d5c06bf08
commit db24d21621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 2591 additions and 12398 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,86 +0,0 @@
/*
Copyright (C) 2011 by MarkLogic Corporation
Author: Mike Brevoort <mike@brevoort.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
.cm-s-xq.CodeMirror { border-radius: 1.5rem; border: 1px solid #d9d9d9; height: auto; }
.cm-s-xq.CodeMirror:hover { background-color: rgb(232 244 242); border-color: #18947b; transition: all .3s; }
.cm-s-xq .CodeMirror-gutters { border-right: 1px solid #ddd; background-color: rgb(221 221 221 / 20%); white-space: nowrap; }
.cm-s-xq span.cm-keyword { line-height: 1em; font-weight: bold; color: #5A5CAD; }
.cm-s-xq span.cm-atom { color: #7A316F; font-weight:bold; }
.cm-s-xq span.cm-number { color: #e36209; }
.cm-s-xq span.cm-def { text-decoration:underline; }
.cm-s-xq span.cm-variable { color: black; }
.cm-s-xq span.cm-variable-2 { color:black; }
.cm-s-xq span.cm-variable-3, .cm-s-xq span.cm-type { color: black; }
.cm-s-xq span.cm-property { color: #008771; }
.cm-s-xq span.cm-operator {}
.cm-s-xq span.cm-comment { color: #bbbbbb; font-style: italic; }
.cm-s-xq span.cm-string {}
.cm-s-xq span.cm-meta { color: yellow; }
.cm-s-xq span.cm-qualifier { color: grey; }
.cm-s-xq span.cm-builtin { color: #7EA656; }
.cm-s-xq span.cm-bracket { color: #cc7; }
.cm-s-xq span.cm-tag { color: #3F7F7F; }
.cm-s-xq span.cm-attribute { color: #7F007F; }
.cm-s-xq span.cm-error { color: #e04141; }
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
.dark .cm-s-xq.CodeMirror { background-color: var(--dark-color-surface-200); border-color: var(--dark-color-surface-300); color: rgb(255 255 255 / 65%); }
.dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 30%); border-color: #008771; transition: all .3s; }
.dark .cm-s-xq div.CodeMirror-selected { background: var(--dark-color-codemirror-line-selection); }
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: var(--dark-color-codemirror-line-selection); }
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: var(--dark-color-codemirror-line-selection); }
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid var(--dark-color-surface-300); }
.dark .cm-s-xq .CodeMirror-guttermarker { color: #FFBD40; }
.dark .cm-s-xq .CodeMirror-guttermarker-subtle { color: rgb(255 255 255 / 70%); }
.dark .cm-s-xq .CodeMirror-linenumber { color: rgb(255 255 255 / 50%); }
.dark .cm-s-xq .CodeMirror-cursor { border-left: 1px solid white; }
.dark .cm-s-xq span.cm-keyword { color: #FFBD40; }
.dark .cm-s-xq span.cm-atom { color: #c099ff; }
.dark .cm-s-xq span.cm-number { color: #9ccfd8; }
.dark .cm-s-xq span.cm-def { color: #FFF; text-decoration:underline; }
.dark .cm-s-xq span.cm-variable { color: #FFF; }
.dark .cm-s-xq span.cm-variable-2 { color: #EEE; }
.dark .cm-s-xq span.cm-variable-3, .dark .cm-s-xq span.cm-type { color: #DDD; }
.dark .cm-s-xq span.cm-property { color: #f6c177; }
.dark .cm-s-xq span.cm-operator {}
.dark .cm-s-xq span.cm-comment { color: gray; }
.dark .cm-s-xq span.cm-string {}
.dark .cm-s-xq span.cm-meta { color: yellow; }
.dark .cm-s-xq span.cm-qualifier { color: #FFF700; }
.dark .cm-s-xq span.cm-builtin { color: #30a; }
.dark .cm-s-xq span.cm-bracket { color: #cc7; }
.dark .cm-s-xq span.cm-tag { color: #FFBD40; }
.dark .cm-s-xq span.cm-attribute { color: #FFF700; }
.dark .cm-s-xq span.cm-error { color: #e04141; }
.dark .cm-s-xq .CodeMirror-activeline-background { background: #27282E; }
.dark .cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }
.Line-Hover{transition: all .2s;}
.Line-Hover:hover{ background-color: rgba(0, 102, 85, 0.05) !important; }
.dark .Line-Hover:hover{ background-color: var(--dark-color-codemirror-line-hover) !important; }
.CodeMirror-foldmarker { color: #fc8800; text-shadow: #ffd8aa 1px 1px 2px, #ffd8aa -1px -1px 2px, #ffd8aa 1px -1px 2px, #ffd8aa -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
.dark .CodeMirror-foldmarker { color: #ffffff; text-shadow: #bbb 1px 1px 2px, #bbb -1px -1px 2px, #bbb 1px -1px 2px, #bbb -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }

1
web/assets/codemirror/xq.min.css vendored Normal file
View file

@ -0,0 +1 @@
.CodeMirror{background-color:#f6fbfa;border:1px solid #d9d9d9}.cm-s-xq.CodeMirror{border-radius:1.5rem;height:auto;transition:background-color .3s,border-color 0.3s}.cm-s-xq.CodeMirror:hover{background-color:#e8f4f2;border-color:#18947b}.cm-s-xq div.CodeMirror-selected{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-line::selection,.cm-s-xq .CodeMirror-line>span::selection,.cm-s-xq .CodeMirror-line>span>span::selection{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-line::-moz-selection,.cm-s-xq .CodeMirror-line>span::-moz-selection,.cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:rgb(0 102 85 / .1)}.cm-s-xq .CodeMirror-gutters{border-right:1px solid #ddd;background-color:rgb(221 221 221 / 20%);white-space:nowrap}.cm-s-xq span.cm-keyword{line-height:1em;font-weight:700;color:#5A5CAD}.cm-s-xq span.cm-atom{color:#7A316F;font-weight:700}.cm-s-xq span.cm-number{color:#e36209}.cm-s-xq span.cm-def{text-decoration:underline}.cm-s-xq span.cm-variable{color:#000}.cm-s-xq span.cm-variable-2{color:#000}.cm-s-xq span.cm-variable-3,.cm-s-xq span.cm-type{color:#000}.cm-s-xq span.cm-property{color:#008771}.cm-s-xq span.cm-comment{color:#bbb;font-style:italic}.cm-s-xq span.cm-meta{color:#ff0}.cm-s-xq span.cm-qualifier{color:grey}.cm-s-xq span.cm-builtin{color:#7EA656}.cm-s-xq span.cm-bracket{color:#cc7}.cm-s-xq span.cm-tag{color:#3F7F7F}.cm-s-xq span.cm-attribute{color:#7F007F}.cm-s-xq span.cm-error{color:#e04141}.cm-s-xq .CodeMirror-activeline-background{background:#e8f2ff}.cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:black!important;background:#ff0}.dark .CodeMirror{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300)}.dark .cm-s-xq.CodeMirror{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300);color:rgb(255 255 255 / 65%)}.dark .cm-s-xq.CodeMirror:hover{background-color:rgb(0 50 42 / 30%);border-color:#008771}.dark .cm-s-xq div.CodeMirror-selected{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-line::selection,.dark .cm-s-xq .CodeMirror-line>span::selection,.dark .cm-s-xq .CodeMirror-line>span>span::selection{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-line::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:var(--dark-color-codemirror-line-selection)}.dark .cm-s-xq .CodeMirror-gutters{background:rgb(0 0 0 / 30%);border-right:1px solid var(--dark-color-surface-300)}.dark .cm-s-xq .CodeMirror-guttermarker{color:#FFBD40}.dark .cm-s-xq .CodeMirror-guttermarker-subtle{color:rgb(255 255 255 / 70%)}.dark .cm-s-xq .CodeMirror-linenumber{color:rgb(255 255 255 / 50%)}.dark .cm-s-xq .CodeMirror-cursor{border-left:1px solid #fff}.dark .cm-s-xq span.cm-keyword{color:#FFBD40}.dark .cm-s-xq span.cm-atom{color:#c099ff}.dark .cm-s-xq span.cm-number{color:#9ccfd8}.dark .cm-s-xq span.cm-def{color:#FFF;text-decoration:underline}.dark .cm-s-xq span.cm-variable{color:#FFF}.dark .cm-s-xq span.cm-variable-2{color:#EEE}.dark .cm-s-xq span.cm-variable-3,.dark .cm-s-xq span.cm-type{color:#DDD}.dark .cm-s-xq span.cm-property{color:#f6c177}.dark .cm-s-xq span.cm-comment{color:gray}.dark .cm-s-xq span.cm-meta{color:#ff0}.dark .cm-s-xq span.cm-qualifier{color:#FFF700}.dark .cm-s-xq span.cm-builtin{color:#30a}.dark .cm-s-xq span.cm-bracket{color:#cc7}.dark .cm-s-xq span.cm-tag{color:#FFBD40}.dark .cm-s-xq span.cm-attribute{color:#FFF700}.dark .cm-s-xq span.cm-error{color:#e04141}.dark .cm-s-xq .CodeMirror-activeline-background{background:#27282E}.dark .cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:white!important}.Line-Hover{transition:all .2s}.CodeMirror pre.CodeMirror-line:hover,.CodeMirror pre.CodeMirror-line-like:hover{background-color:rgb(0 102 85 / .05)}.dark .CodeMirror pre.CodeMirror-line:hover,.CodeMirror pre.CodeMirror-line-like:hover{background-color:var(--dark-color-codemirror-line-hover)}.CodeMirror-foldmarker{color:#fc8800;text-shadow:#ffd8aa 1px 1px 2px,#ffd8aa -1px -1px 2px,#ffd8aa 1px -1px 2px,#ffd8aa -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.dark .CodeMirror-foldmarker{color:#fff;text-shadow:#bbb 1px 1px 2px,#bbb -1px -1px 2px,#bbb 1px -1px 2px,#bbb -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}

File diff suppressed because one or more lines are too long

1
web/assets/css/custom.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/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 }}">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
<style>
[v-cloak] {
display: none;

View file

@ -1,27 +1,41 @@
{{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:dialog-style="{ top: '20px' }"
:dialog-style="isMobileQr ? { top: '18px' } : {}"
:closable="true"
:class="themeSwitcher.currentTheme"
:footer="null" width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.clickOnQRcode" }}
</a-tag>
:footer="null" width="fit-content">
<tr-qr-modal class="qr-modal">
<template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas></div>
<a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas></div>
<tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
<tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner class="qr-bg-sub-inner">
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
<tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
<tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner class="qr-bg-sub-inner">
<canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
</template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas></div>
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
</tr-qr-modal>
</a-modal>
<script>
const isMobileQr = window.innerWidth <= 768;
const qrModal = {
title: '',
dbInbound: new DBInbound(),
@ -30,17 +44,17 @@
clipboard: null,
visible: false,
subId: '',
show: function (title = '', dbInbound, client) {
show: function(title = '', dbInbound, client) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.client = client;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD){
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l,index) =>{
if (this.inbound.protocol == Protocols.WIREGUARD) {
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
this.qrcodes.push({
remark: "Peer " + (index+1),
remark: "Peer " + (index + 1),
link: l
});
});
@ -54,11 +68,10 @@
}
this.visible = true;
},
close: function () {
close: function() {
this.visible = false;
},
};
const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal',
@ -88,13 +101,28 @@
});
},
genSubLink(subID) {
return app.subSettings.subURI+subID;
return app.subSettings.subURI + subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
return app.subSettings.subJsonURI + subID;
},
revertOverflow() {
const elements = document.querySelectorAll(".qr-tag");
elements.forEach((element) => {
element.classList.remove("tr-marquee");
element.children[0].style.animation = '';
while (element.children.length > 1) {
element.removeChild(element.lastChild);
}
});
}
},
updated() {
if (this.qrModal.visible) {
fixOverflow();
} else {
this.revertOverflow();
}
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
@ -106,5 +134,32 @@
}
});
function fixOverflow() {
const elements = document.querySelectorAll(".qr-tag");
elements.forEach((element) => {
function isElementOverflowing(element) {
const overflowX = element.offsetWidth < element.scrollWidth,
overflowY = element.offsetHeight < element.scrollHeight;
return overflowX || overflowY;
}
function wrapContentsInMarquee(element) {
element.classList.add("tr-marquee");
element.children[0].style.animation = `move-ltr ${
(element.children[0].clientWidth / element.clientWidth) * 5
}s ease-in-out infinite`;
const marqueeText = element.children[0];
if (element.children.length < 2) {
for (let i = 0; i < 1; i++) {
const marqueeText = element.children[0].cloneNode(true);
element.children[0].after(marqueeText);
}
}
}
if (isElementOverflowing(element)) {
wrapContentsInMarquee(element);
}
});
}
</script>
{{end}}

View file

@ -400,7 +400,7 @@
</g>
</svg>
</div>
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto; overflow-x: hidden;">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
<a-row type="flex" justify="center">
<a-col style="width: 100%;">
@ -461,7 +461,7 @@
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch></theme-switch>
<theme-switch-login></theme-switch-login>
</a-row>
</a-form-item>
</a-form>
@ -482,7 +482,6 @@
this.password = "";
}
}
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',

View file

@ -1,46 +1,50 @@
{{define "menuItems"}}
<a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon>
<span><b>{{ i18n "menu.dashboard"}}</b></span>
<span>
<b>{{ i18n "menu.dashboard"}}</b>
</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon>
<span><b>{{ i18n "menu.inbounds"}}</b></span>
<span>
<b>{{ i18n "menu.inbounds"}}</b>
</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/settings">
<a-icon type="setting"></a-icon>
<span><b>{{ i18n "menu.settings"}}</b></span>
<span>
<b>{{ i18n "menu.settings"}}</b>
</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}panel/xray">
<a-icon type="tool"></a-icon>
<span><b>{{ i18n "menu.xray"}}</b></span>
<span>
<b>{{ i18n "menu.xray"}}</b>
</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon>
<span><b>{{ i18n "menu.logout"}}</b></span>
<span>
<b>{{ i18n "menu.logout"}}</b>
</span>
</a-menu-item>
{{end}}
{{define "commonSider"}}
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md">
<theme-switch></theme-switch>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
<a-menu :theme="themeSwitcher.currentTheme" 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-class-name="themeSwitcher.currentTheme"
:wrap-style="{ padding: 0 }">
<a-drawer id="sider-drawer" placement="left" :closable="false" @close="siderDrawer.close()" :visible="siderDrawer.visible" :wrap-class-name="themeSwitcher.currentTheme" :wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div>
<theme-switch></theme-switch>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']" @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}}
</a-menu>
</a-drawer>

View file

@ -1,5 +1,5 @@
{{define "component/sortableTableTrigger"}}
<a-icon type="drag"
<a-icon type="drag"
class="sortable-icon"
style="cursor: move;"
@mouseup="mouseUpHandler"
@ -10,7 +10,6 @@
{{define "component/sortableTable"}}
<script>
const DRAGGABLE_ROW_CLASS = 'draggable-row';
const findParentRowElement = (el) => {
if (!el || !el.tagName) {
return null;
@ -22,7 +21,6 @@
return null;
}
}
Vue.component('a-table-sortable', {
data() {
return {
@ -34,22 +32,19 @@
inheritAttrs: false,
provide() {
const sortable = {}
Object.defineProperty(sortable, "setSortableIndex", {
enumerable: true,
get: () => this.setCurrentSortableIndex,
});
Object.defineProperty(sortable, "resetSortableIndex", {
enumerable: true,
get: () => this.resetSortableIndex,
});
return {
sortable,
}
},
render: function (createElement) {
render: function(createElement) {
return createElement('a-table', {
class: {
'ant-table-is-sorting': this.isDragging(),
@ -101,23 +96,18 @@
if (!this.isDragging()) {
return;
}
e.preventDefault();
const currentIndex = this.sortingElementIndex;
if (index === currentIndex) {
this.newElementIndex = null;
return;
}
const row = findParentRowElement(e.target);
if (!row) {
return;
}
const rect = row.getBoundingClientRect();
const offsetTop = e.pageY - rect.top;
if (offsetTop < rect.height / 2) {
this.newElementIndex = Math.max(index - 1, 0);
} else {
@ -133,7 +123,6 @@
const parentMethodResult = this.customRow?.(record, index) || {};
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
return {
...parentMethodResult,
attrs: {
@ -149,9 +138,7 @@
class: {
...(parentMethodResult?.class || {}),
[DRAGGABLE_ROW_CLASS]: true,
['dragging']: this.isDragging()
? (newIndex === null ? index === currentIndex : index === newIndex)
: false,
['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false,
},
};
}
@ -160,28 +147,22 @@
records() {
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
return this.dataSource;
}
if (this.$memoSort.newIndex === newIndex) {
return this.$memoSort.list;
}
let list = [...this.dataSource];
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
this.$memoSort = {
newIndex,
list,
};
return list;
}
}
});
Vue.component('table-sort-trigger', {
template: `{{template "component/sortableTableTrigger"}}`,
props: ['item-index'],
@ -203,7 +184,6 @@
}
})
</script>
<style>
@media only screen and (max-width: 767px) {
.sortable-icon {

View file

@ -1,6 +1,23 @@
{{define "component/themeSwitchTemplate"}}
<template>
<a-menu class="change-theme" :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-sub-menu>
<span slot="title">
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<span>Theme</span>
</span>
<a-menu-item id="change-theme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOff()"> Dark <a-switch style="margin-left: 2px;" size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
</a-menu-item>
<a-menu-item id="change-theme-ultra" v-if="themeSwitcher.isDarkTheme" class="ant-menu-theme-switch" @mousedown="themeSwitcher.animationsOffUltra()"> Ultra <a-checkbox style="margin-left: 2px;" :checked="themeSwitcher.isUltra" @click="themeSwitcher.toggleUltra()"></a-checkbox>
</a-menu-item>
</a-sub-menu>
</a-menu>
</template>
{{end}}
{{define "component/themeSwitchTemplateLogin"}}
<template>
<a-menu @mousedown="themeSwitcher.animationsOff()" id="change-theme" :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline" class="ant-menu-theme-switch">
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
@ -23,6 +40,26 @@
const theme = isDarkTheme ? 'dark' : 'light';
document.querySelector('body').setAttribute('class', theme);
return {
animationsOff() {
document.documentElement.setAttribute('data-theme-animations', 'off');
const themeAnimations = document.querySelector('#change-theme');
themeAnimations.addEventListener('mouseleave', () => {
document.documentElement.removeAttribute('data-theme-animations');
});
themeAnimations.addEventListener('touchend', () => {
document.documentElement.removeAttribute('data-theme-animations');
});
},
animationsOffUltra() {
document.documentElement.setAttribute('data-theme-animations', 'off');
const themeAnimationsUltra = document.querySelector('#change-theme-ultra');
themeAnimationsUltra.addEventListener('mouseleave', () => {
document.documentElement.removeAttribute('data-theme-animations');
});
themeAnimationsUltra.addEventListener('touchend', () => {
document.documentElement.removeAttribute('data-theme-animations');
});
},
isDarkTheme,
isUltra,
get currentTheme() {
@ -57,13 +94,19 @@
getContainer: () => document.getElementById('message')
});
document.getElementById('message').className = themeSwitcher.currentTheme;
const themeAnimations = document.querySelector('.change-theme');
themeAnimations.addEventListener('mousedown', () => {
document.documentElement.setAttribute('data-theme-animations', 'off');
}
});
themeAnimations.addEventListener('mouseleave', () => {
document.documentElement.removeAttribute('data-theme-animations');
Vue.component('theme-switch-login', {
props: [],
template: `{{template "component/themeSwitchTemplateLogin"}}`,
data: () => ({
themeSwitcher
}),
mounted() {
this.$message.config({
getContainer: () => document.getElementById('message')
});
document.getElementById('message').className = themeSwitcher.currentTheme;
}
});
</script>

View file

@ -1,27 +1,20 @@
{{define "dnsModal"}}
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok"
:closable="true" :mask-closable="false"
:ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok" :closable="true" :mask-closable="false" :ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
<a-button size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')">+</a-button>
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')"></a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
<a-button size="small" slot="addonAfter" @click="dnsModal.dnsServer.domains.splice(index,1)">-</a-button>
<a-button icon="minus" size="small" slot="addonAfter" @click="dnsModal.dnsServer.domains.splice(index,1)"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
<a-select
v-model="dnsModal.dnsServer.queryStrategy"
style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
[[ l ]]
</a-select-option>
<a-select v-model="dnsModal.dnsServer.queryStrategy" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
</a-select>
</a-form-item>
</a-form>
@ -39,18 +32,24 @@
queryStrategy: 'UseIP',
},
ok() {
domains = dnsModal.dnsServer.domains.filter(d => d.length>0);
domains = dnsModal.dnsServer.domains.filter(d => d.length > 0);
dnsModal.dnsServer.domains = domains;
newDnsServer = domains.length > 0 ? dnsModal.dnsServer : dnsModal.dnsServer.address;
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
},
show({ title='', okText='{{ i18n "confirm" }}', dnsServer, confirm=(dnsServer)=>{}, isEdit=false }) {
show({
title = '',
okText = '{{ i18n "confirm" }}',
dnsServer,
confirm = (dnsServer) => {},
isEdit = false
}) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
if (typeof dnsServer == 'object'){
if (isEdit) {
if (typeof dnsServer == 'object') {
this.dnsServer = dnsServer;
} else {
this.dnsServer = {
@ -72,7 +71,6 @@
dnsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#dns-modal',
@ -81,10 +79,11 @@
},
computed: {
isAdvanced: {
get: function () { return dnsModal.dnsServer.domains.length>0 }
get: function() {
return dnsModal.dnsServer.domains.length > 0
}
}
}
});
</script>
{{end}}

View file

@ -1,8 +1,8 @@
{{define "form/outbound"}}
<!-- base -->
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
<a-tab-pane key="1" tab="Form">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-tab-pane key="1" tab="Form">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
@ -15,26 +15,19 @@
<a-input v-model="outbound.sendThrough"></a-input>
</a-form-item>
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
<a-form-item label='Strategy'>
<a-select
v-model="outbound.settings.domainStrategy"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Fragment'>
<a-switch
:checked="Object.keys(outbound.settings.fragment).length >0"
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
</a-switch>
<a-switch :checked="Object.keys(outbound.settings.fragment).length >0" @change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}"></a-switch>
</a-form-item>
<template v-if="Object.keys(outbound.settings.fragment).length >0">
<a-form-item label='Packets'>
<a-select
v-model="outbound.settings.fragment.packets"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
@ -45,39 +38,36 @@
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item>
</template>
</template>
</template>
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<a-form-item label='Response Type'>
<a-select
v-model="outbound.settings.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</template>
</template>
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select
v-model="outbound.settings.network"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</template>
</template>
<!-- wireguard settings -->
<template v-if="outbound.protocol === Protocols.Wireguard">
<!-- wireguard settings -->
<template v-if="outbound.protocol === Protocols.Wireguard">
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
{{ i18n "pages.xray.outbound.address" }} <a-icon type="question-circle"></a-icon>
{{ i18n "pages.xray.outbound.address" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="outbound.settings.address"></a-input>
@ -89,9 +79,7 @@
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon type="sync"
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
</a-icon>
<a-icon type="sync" @click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="outbound.settings.secretKey"></a-input>
@ -108,7 +96,7 @@
<a-input-number v-model.number="outbound.settings.mtu"></a-input-number>
</a-form-item>
<a-form-item label='Workers'>
<a-input-number min="0" v-model.number="outbound.settings.workers"></a-input>
<a-input-number min="0" v-model.number="outbound.settings.workers"></a-input-number>
</a-form-item>
<a-form-item label='Kernel Mode'>
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
@ -118,22 +106,17 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Reserved <a-icon type="question-circle"></a-icon>
</template> Reserved <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item>
<a-form-item label="Peers">
<a-button type="primary" size="small" @click="outbound.settings.addPeer()">+</a-button>
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button>
</a-form-item>
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Peer [[ index + 1 ]]
<a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
<a-divider style="margin:0;"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
<a-input v-model.trim="peer.endpoint"></a-input>
</a-form-item>
@ -145,35 +128,37 @@
</a-form-item>
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
{{ i18n "pages.xray.wireguard.allowedIPs" }}
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
</template>
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
<a-input v-model.trim="peer.allowedIPs[index]">
<a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='Keep Alive'>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
</a-form-item>
</a-form>
</template>
</template>
<!-- Address + Port -->
<template v-if="outbound.hasAddressPort()">
<!-- Address + Port -->
<template v-if="outbound.hasAddressPort()">
<a-form-item label='{{ i18n "pages.inbounds.address" }}'>
<a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
</a-form-item>
</template>
</template>
<!-- Vnext (vless/vmess) settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<!-- Vnext (vless/vmess) settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item>
<!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'>
@ -183,10 +168,10 @@
</a-select>
</a-form-item>
</template>
</template>
</template>
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
<template v-if="outbound.hasServers()">
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
<template v-if="outbound.hasServers()">
<!-- http / socks -->
<template v-if="outbound.hasUsername()">
<a-form-item label='{{ i18n "username" }}'>
@ -196,12 +181,14 @@
<a-input v-model.trim="outbound.settings.pass"></a-input>
</a-form-item>
</template>
<!-- trojan/shadowsocks -->
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<!-- trojan/shadowsocks -->
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.settings.password"></a-input>
</a-form-item>
</template>
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'>
@ -213,13 +200,12 @@
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
</template>
</template>
</template>
<!-- stream settings -->
<template v-if="outbound.canEnableStream()">
<!-- stream settings -->
<template v-if="outbound.canEnableStream()">
<a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
@ -231,9 +217,7 @@
</a-form-item>
<template v-if="outbound.stream.network === 'tcp'">
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch :checked="outbound.stream.tcp.type === 'http'"
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
<a-switch :checked="outbound.stream.tcp.type === 'http'" @change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
</a-form-item>
<template v-if="outbound.stream.tcp.type == 'http'">
<a-form-item label='{{ i18n "host" }}'>
@ -350,10 +334,10 @@
<a-input v-model.trim="outbound.stream.httpupgrade.path"></a-input>
</a-form-item>
</template>
</template>
</template>
<!-- tls settings -->
<template v-if="outbound.canEnableTls()">
<!-- tls settings -->
<template v-if="outbound.canEnableTls()">
<a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="outbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
@ -366,16 +350,13 @@
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
@ -390,8 +371,7 @@
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
@ -405,9 +385,9 @@
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
</a-form-item>
</template>
</template>
</template>
<!-- sockopt settings -->
<!-- sockopt settings -->
<a-form-item label="Sockopts">
<a-switch v-model="outbound.stream.sockoptSwitch"></a-switch>
</a-form-item>
@ -426,10 +406,10 @@
<a-form-item label="TCP No-Delay">
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
</a-form-item>
</template>
</template>
<!-- mux settings -->
<template v-if="outbound.canEnableMux()">
<!-- mux settings -->
<template v-if="outbound.canEnableMux()">
<a-form-item label="Mux">
<a-switch v-model="outbound.mux.enabled"></a-switch>
</a-form-item>
@ -446,16 +426,16 @@
</a-select>
</a-form-item>
</template>
</template>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0">
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
</template>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0"> Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
<a-button @click="convertLink" type="primary">
<a-icon type="form"></a-icon>
</a-button>
</a-form-item>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
</a-tab-pane>
</a-tab-pane>
</a-tabs>
{{end}}

View file

@ -4,7 +4,9 @@
<tr>
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())">+</a-button></td>
<td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
</td>
</tr>
</table>
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
@ -13,7 +15,7 @@
</a-input>
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
</template>
</a-input>
</a-input-group>

View file

@ -7,15 +7,16 @@
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
<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'">
<table style="width: 100%; text-align: center; margin: 1rem 0;">
<tr>
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
<td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button>
</td>
</tr>
</table>
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
@ -24,7 +25,7 @@
</a-input>
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
</template>
</a-input>
</a-input-group>

View file

@ -21,17 +21,13 @@
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Fallback [[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;" />
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>

View file

@ -23,17 +23,13 @@
<template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Fallback [[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;" />
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>

View file

@ -7,9 +7,7 @@
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon type="sync"
@click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
</a-icon>
<a-icon type="sync" @click="[inbound.settings.pubKey, inbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.settings.secretKey"></a-input>
@ -24,13 +22,10 @@
<a-switch v-model="inbound.settings.kernelMode"></a-switch>
</a-form-item>
<a-form-item label="Peers">
<a-button type="primary" size="small" @click="inbound.settings.addPeer()">+</a-button>
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addPeer()"></a-button>
</a-form-item>
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Peer [[ index + 1 ]]
<a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
<a-divider style="margin:0;"> Peer [[ index + 1 ]] <a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
</a-divider>
<a-form-item>
<template slot="label">
@ -39,7 +34,7 @@
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())"type="sync"> </a-icon>
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="peer.privateKey"></a-input>
@ -57,23 +52,24 @@
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.psk" }}
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())"type="sync"> </a-icon>
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="peer.psk"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
{{ i18n "pages.xray.wireguard.allowedIPs" }}
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
</template>
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
<a-input v-model.trim="peer.allowedIPs[index]">
<a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='Keep Alive'>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
</a-form-item>
</a-form>
</a-form>

View file

@ -3,7 +3,7 @@
<a-divider style="margin:5px 0 0;"></a-divider>
<a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch>
<a-button v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})">+</a-button>
<a-button icon="plus" v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
</a-form-item>
<a-input-group style="margin: 8px 0;" compact v-for="(row, index) in inbound.stream.externalProxy">
<template>
@ -19,8 +19,11 @@
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
</a-tooltip>
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
<a-button style="width: 10%; margin: 0px; border-radius: 0 1rem 1rem 0;" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
<a-input style="width: 30%; top: 0;" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonAfter">
<a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
</template>
</a-input>
</a-input-group>
</a-form>
{{end}}

View file

@ -5,13 +5,11 @@
</a-form-item>
<a-form-item>
<template slot="label">{{ i18n "host" }}
<a-button size="small" @click="inbound.stream.http.addHost()">+</a-button>
<a-button icon="plus" size="small" @click="inbound.stream.http.addHost()"></a-button>
</template>
<template v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]">
<a-button size="small" slot="addonAfter"
@click="inbound.stream.http.removeHost(index)"
v-if="inbound.stream.http.host.length>1">-</a-button>
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.http.removeHost(index)" v-if="inbound.stream.http.host.length>1"></a-button>
</a-input>
</template>
</a-form-item>

View file

@ -10,7 +10,7 @@
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')">+</a-button>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
@ -18,7 +18,7 @@
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.httpupgrade.removeHeader(index)">-</a-button>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>

View file

@ -5,14 +5,11 @@
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch :checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
<a-switch :checked="inbound.stream.tcp.type === 'http'" @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
</a-form-item>
</a-form>
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<!-- tcp request -->
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
@ -23,28 +20,24 @@
</a-form-item>
<a-form-item>
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
<a-button size="small" @click="inbound.stream.tcp.request.addPath('/')">+</a-button>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
</template>
<template v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
<a-button size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)" v-if="inbound.stream.tcp.request.path.length>1"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">+</a-button>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
@ -61,19 +54,16 @@
<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-button size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
<a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
</template>
</a-input>
</a-input-group>

View file

@ -10,7 +10,7 @@
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.ws.addHeader('host', '')">+</a-button>
<a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
@ -18,7 +18,7 @@
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>

View file

@ -43,15 +43,13 @@
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
@ -67,10 +65,8 @@
<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-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"
style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px"></a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
@ -80,8 +76,7 @@
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n
"pages.inbounds.setDefaultCert" }}</a-button>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
@ -104,8 +99,7 @@
<a-input v-model.trim="inbound.stream.xtls.sni"></a-input>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.xtls.alpn">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.xtls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
@ -118,10 +112,8 @@
<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-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()"
style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small"
@click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px"></a-button>
<a-button icon="minus" v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px"></a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
@ -131,8 +123,7 @@
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n
"pages.inbounds.setDefaultCert" }}</a-button>
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
@ -145,6 +136,7 @@
</template>
</template>
</template>
<!-- reality settings -->
<template v-if="inbound.stream.isReality">
<a-form-item label='Show'>
@ -154,8 +146,7 @@
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
@ -170,10 +161,8 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
Short ID
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
</a-icon>
</template> Short ID <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</a-form-item>

View file

@ -14,23 +14,16 @@
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
:overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "reset"}}'
cancel-text='{{ i18n "cancel"}}'>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
</a-popconfirm>
</a-tooltip>
<a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-popconfirm @confirm="delClient(record.id,client,false)"
title='{{ i18n "pages.inbounds.deleteClientContent"}}'
:overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "delete"}}'
ok-type="danger"
cancel-text='{{ i18n "cancel"}}'>
<template slot="title">
<span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
</template>
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon>
<a-icon style="font-size: 24px; cursor: pointer;" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
</a-popconfirm>
@ -54,10 +47,8 @@
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
</a-badge>
</a-tooltip>
[[ client.email ]]
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</a-tooltip> [[ client.email ]]
</template>
<template slot="traffic" slot-scope="text, client">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
@ -74,29 +65,20 @@
</table>
</template>
<table>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ sizeFormat(getSumStats(record, client.email)) ]]
<tr class="tr-table-box">
<td class="tr-table-rt"> [[ sizeFormat(getSumStats(record, client.email)) ]] </td>
<td class="tr-table-bar" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
</td>
<td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
:show-info="false"
:percent="statsProgress(record, client.email)"/>
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
</td>
<td width="120px" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="clientStatsColor(record, client.email)"
:show-info="false"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
<td v-else class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :percent="100"></a-progress>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress
:show-info="false"
:percent="100"></a-progress>
</td>
<td width="60px">
<td class="tr-table-lt">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
<span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
<span v-else class="tr-infinity-ch">&infin;</span>
</td>
</tr>
</table>
@ -106,20 +88,17 @@
<template v-if="client.expiryTime !=0 && client.reset >0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<table>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ remainedDays(client.expiryTime) ]]
<tr class="tr-table-box">
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
<td class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
</td>
<td width="120px" class="infinite-bar">
<a-progress :show-info="false"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
</tr>
</table>
</a-popover>
@ -127,14 +106,17 @@
<template v-else>
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
[[ remainedDays(client.expiryTime) ]]
</a-tag>
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
</a-popover>
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">&infin;</a-tag>
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</template>
</template>
<template slot="actionMenu" slot-scope="text, client, index">
@ -162,8 +144,7 @@
<span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
</a-menu-item>
<a-menu-item>
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)">
</a-switch>
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)"></a-switch>
{{ i18n "enable"}}
</a-menu-item>
</a-menu>
@ -177,13 +158,9 @@
<td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td>
</tr>
<tr>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]]
</td>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;"> [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
<td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
:show-info="false"
:percent="statsProgress(record, client.email)"/>
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
</td>
<td width="120px" v-else-if="client.totalGB > 0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
@ -199,20 +176,15 @@
</tr>
</table>
</template>
<a-progress :stroke-color="clientStatsColor(record, client.email)"
:show-info="false"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
</a-popover>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'"
:show-info="false"
:percent="100"></a-progress>
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false" :percent="100"></a-progress>
</td>
<td width="80px">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
<span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
<span v-else class="tr-infinity-ch">&infin;</span>
</td>
</tr>
<tr>
@ -223,18 +195,15 @@
</tr>
<tr>
<template v-if="client.expiryTime !=0 && client.reset >0">
<td width="80px" style="margin:0; text-align: right;font-size: 1em;">
[[ remainedDays(client.expiryTime) ]]
</td>
<td width="80px" style="margin:0; text-align: right;font-size: 1em;"> [[ remainedDays(client.expiryTime) ]] </td>
<td width="120px" class="infinite-bar">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-progress :show-info="false"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
</a-popover>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
@ -243,15 +212,17 @@
<td colspan="3" style="text-align: center;">
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
</span>
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-tag style="min-width: 50px; border: none;"
:color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
[[ remainedDays(client.expiryTime) ]]
</a-tag>
<a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
</a-popover>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">&infin;</a-tag>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</template>
</td>
</tr>
@ -259,7 +230,9 @@
</template>
<a-badge>
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"><a-icon type="solution"></a-icon></a-button>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
<a-icon type="solution"></a-icon>
</a-button>
</a-badge>
</a-popover>
</template>

View file

@ -1,29 +1,38 @@
{{define "inboundInfoModal"}}
<a-modal id="inbound-info-modal"
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true"
:mask-closable="true"
:footer="null"
width="600px"
:class="themeSwitcher.currentTheme"
>
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' :closable="true" :mask-closable="true" :footer="null" width="600px" :class="themeSwitcher.currentTheme">
<a-row>
<a-col :xs="24" :md="12">
<table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="purple">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td>
<tr>
<td>{{ i18n "protocol" }}</td>
<td>
<a-tag color="purple">[[ dbInbound.protocol ]]</a-tag>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.address" }}</td>
<td>
<a-tooltip :title="[[ dbInbound.address ]]">
<a-tag class="info-large-tag">[[ dbInbound.address ]]</a-tag>
</a-tooltip>
</td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag>[[ dbInbound.port ]]</a-tag></td></tr>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td>
<a-tag>[[ dbInbound.port ]]</a-tag>
</td>
</tr>
</table>
</a-col>
<a-col :xs="24" :md="12">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<table>
<tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
<td>{{ i18n "transmission" }}</td>
<td>
<a-tag color="green">[[ inbound.network ]]</a-tag>
</td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2 || inbound.isHttpupgrade ">
<tr>
@ -33,7 +42,10 @@
<a-tag class="info-large-tag">[[ inbound.host ]]</a-tag>
</a-tooltip>
</td>
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<td v-else>
<a-tag color="orange">{{ i18n "none" }}</a-tag>
</td>
</tr>
</tr>
<tr>
<td>{{ i18n "path" }}</td>
@ -41,27 +53,58 @@
<a-tooltip :title="[[ inbound.path ]]">
<a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
</a-tooltip>
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td>
<td v-else>
<a-tag color="orange">{{ i18n "none" }}</a-tag>
</td>
</tr>
</template>
<template v-if="inbound.isQuic">
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag>[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag>[[ inbound.quicType ]]</a-tag></td></tr>
<tr>
<td>quic {{ i18n "encryption" }}</td>
<td>
<a-tag>[[ inbound.quicSecurity ]]</a-tag>
</td>
</tr>
<tr>
<td>quic {{ i18n "password" }}</td>
<td>
<a-tag>[[ inbound.quicKey ]]</a-tag>
</td>
</tr>
<tr>
<td>quic {{ i18n "camouflage" }}</td>
<td>
<a-tag>[[ inbound.quicType ]]</a-tag>
</td>
</tr>
</template>
<template v-if="inbound.isKcp">
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag>[[ inbound.kcpSeed ]]</a-tag></td></tr>
<tr>
<td>kcp {{ i18n "encryption" }}</td>
<td>
<a-tag>[[ inbound.kcpType ]]</a-tag>
</td>
</tr>
<tr>
<td>kcp {{ i18n "password" }}</td>
<td>
<a-tag>[[ inbound.kcpSeed ]]</a-tag>
</td>
</tr>
</template>
<template v-if="inbound.isGrpc">
<tr><td>grpc serviceName</td><td>
<tr>
<td>grpc serviceName</td>
<td>
<a-tooltip :title="[[ inbound.serviceName ]]">
<a-tag class="info-large-tag">[[ inbound.serviceName ]]</a-tag>
</a-tooltip>
<tr><td>grpc multiMode</td><td><a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
<tr>
<td>grpc multiMode</td>
<td>
<a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag>
</td>
</tr>
</template>
</table>
</template>
@ -72,23 +115,30 @@
<br />
<template v-if="inbound.stream.security != 'none'">
{{ i18n "domainName" }}
<a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
<a-tag v-else color="orange">{{ i18n "none" }}</a-tag>
</template>
</template>
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
<tr>
<td>{{ i18n "encryption" }}</td>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
</tr><tr v-if="inbound.isSS2022">
<td>
<a-tag color="green">[[ inbound.settings.method ]]</a-tag>
</td>
</tr>
<tr v-if="inbound.isSS2022">
<td>{{ i18n "password" }}</td>
<td>
<a-tooltip :title="[[ inbound.settings.password ]]">
<a-tag class="info-large-tag">[[ inbound.settings.password ]]</a-tag>
</a-tooltip>
</td>
</tr><tr>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.network" }}</td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td>
<a-tag color="green">[[ inbound.settings.network ]]</a-tag>
</td>
</tr>
</table>
<template v-if="infoModal.clientSettings">
@ -96,19 +146,36 @@
<table style="margin-bottom: 10px;">
<tr>
<td>{{ i18n "pages.inbounds.email" }}</td>
<td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td>
<td v-if="infoModal.clientSettings.email">
<a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag>
</td>
<td v-else>
<a-tag color="red">{{ i18n "none" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.id">
<td>ID</td>
<td><a-tag>[[ infoModal.clientSettings.id ]]</a-tag></td>
<td>
<a-tag>[[ infoModal.clientSettings.id ]]</a-tag>
</td>
</tr>
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
<td>Flow</td>
<td><a-tag>[[ infoModal.clientSettings.flow ]]</a-tag></td>
<td v-if="infoModal.clientSettings.flow">
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
</td>
<td v-else>
<a-tag color="orange">{{ i18n "none" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.inbound.xtls">
<td>Flow</td>
<td><a-tag>[[ infoModal.clientSettings.flow ]]</a-tag></td>
<td v-if="infoModal.clientSettings.flow">
<a-tag>[[ infoModal.clientSettings.flow ]]</a-tag>
</td>
<td v-else>
<a-tag color="orange">{{ i18n "none" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.password">
<td>{{ i18n "password" }}</td>
@ -134,7 +201,7 @@
</td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%; text-align: center;">
<table style="display: inline-table; margin-block: 10px; width: 100%; text-align: center;">
<tr>
<th>{{ i18n "remained" }}</th>
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
@ -142,139 +209,161 @@
</tr>
<tr>
<td>
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ getRemStats() ]]
</a-tag>
<a-tag v-if="infoModal.clientStats && infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ getRemStats() ]] </a-tag>
</td>
<td>
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)"> [[ sizeFormat(infoModal.clientSettings.totalGB) ]] </a-tag>
<a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
<a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
</td>
<td>
<template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag>
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)"> [[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]] </a-tag>
</template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}
</a-tag>
<a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</td>
</tr>
</table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription URL</a-divider>
<a-row>
<a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right;">
<tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag color="purple">Subscription Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
<a-icon type="snippets"></a-icon>
</button>
<a-button size="small" icon="snippets" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-button>
</a-tooltip>
</a-col>
</a-row>
<a-row>
<a-col :sx="24" :md="22">JSON: <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right; margin-top: 5px;">
</tr-info-title>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
</tr-info-row>
<tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag color="purple">Json Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)">
<a-icon type="snippets"></a-icon>
</button>
<a-button size="small" icon="snippets" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)"></a-button>
</a-tooltip>
</a-col>
</a-row>
</tr-info-title>
<a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a>
</tr-info-row>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram ID</a-divider>
<a-row>
<a-col :sx="24" :md="22">[[ infoModal.clientSettings.tgId ]]</a-col>
<a-col :sx="24" :md="2" style="text-align: right;">
<tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag color="blue">[[ infoModal.clientSettings.tgId ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', infoModal.clientSettings.tgId)">
<a-icon type="snippets"></a-icon>
</button>
<a-button size="small" icon="snippets" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', infoModal.clientSettings.tgId)"></a-button>
</a-tooltip>
</a-col>
</a-row>
</tr-info-title>
</tr-info-row>
</template>
<template v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :sx="24" :md="22"><a-tag color="green">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :sx="24" :md="2" style="text-align: right;">
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)"></a-button>
</a-tooltip>
</a-col>
</a-row>
</tr-info-title>
<code>[[ link.link ]]</code>
</tr-info-row>
</template>
</template>
<template v-else>
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="green">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag class="tr-info-tag" color="green">[[ link.remark ]]</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)"></a-button>
</a-tooltip>
</a-col>
</a-row>
</tr-info-title>
<code>[[ link.link ]]</code>
</tr-info-row>
</template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table">
<tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
<tr>
<td>
<a-tag color="green">[[ inbound.settings.address ]]</a-tag>
</td>
<td>
<a-tag color="green">[[ inbound.settings.port ]]</a-tag>
</td>
<td>
<a-tag color="green">[[ inbound.settings.network ]]</a-tag>
</td>
<td>
<a-tag color="green">[[ inbound.settings.followRedirect ]]</a-tag>
</td>
</tr>
</table>
<table v-if="dbInbound.isSocks" style="margin-bottom: 10px; width: 100%;">
<table v-if="dbInbound.isSocks" class="tr-info-table">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
<td>
<a-tag color="green">[[ inbound.settings.auth ]]</a-tag>
</td>
<td>
<a-tag color="green">[[ inbound.settings.udp]]</a-tag>
</td>
<td>
<a-tag color="green">[[ inbound.settings.ip ]]</a-tag>
</td>
</tr>
<template v-if="inbound.settings.auth == 'password'">
<tr>
<td> </td>
<td></td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
</tr>
<tr v-for="account,index in inbound.settings.accounts">
<td>[[ index ]]</td>
<td><a-tag color="green">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
<td>
<a-tag color="green">[[ account.user ]]</a-tag>
</td>
<td>
<a-tag color="green">[[ account.pass ]]</a-tag>
</td>
</tr>
</template>
</table>
<table v-if="dbInbound.isHTTP" style="margin-bottom: 10px; width: 100%;">
<table v-if="dbInbound.isHTTP" class="tr-info-table">
<tr>
<th> </th>
<th></th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
</tr>
<tr v-for="account,index in inbound.settings.accounts">
<td>[[ index ]]</td>
<td><a-tag color="green">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
<td>
<a-tag color="green">[[ account.user ]]</a-tag>
</td>
<td>
<a-tag color="green">[[ account.pass ]]</a-tag>
</td>
</tr>
</table>
<table v-if="dbInbound.isWireguard" style="margin-bottom: 10px; width: 100%;">
<table v-if="dbInbound.isWireguard" class="tr-info-table">
<tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
<td>[[ inbound.settings.secretKey ]]</td>
@ -293,7 +382,9 @@
</tr>
<template v-for="(peer, index) in inbound.settings.peers">
<tr>
<td colspan="2"><a-divider>Peer [[ index + 1 ]]</a-divider></td>
<td colspan="2">
<a-divider>Peer [[ index + 1 ]]</a-divider>
</td>
</tr>
<tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
@ -317,24 +408,16 @@
</tr>
<tr>
<td colspan="2">
<a-row>
<a-col :span="22" style="overflow-wrap: anywhere;">
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row">
<tr-info-title class="tr-info-title">
<a-tag color="blue">Config</a-tag>
<div
v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
style="border-radius: 1rem; padding: 0.5rem;"
class="client-table-odd-row"></div>
</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary"
:id="'copy-url-link-'+index"
@click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])">
<a-icon type="snippets"></a-icon>
</button>
<a-button style="min-width: 24px;" size="small" icon="snippets" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])"></a-button>
</a-tooltip>
</a-col>
</a-row>
</tr-info-title>
<div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)" style="border-radius: 1rem; padding: 0.5rem;" class="client-table-odd-row">
</div>
</tr-info-row>
</td>
</tr>
</table>
@ -361,9 +444,9 @@
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
if (this.inbound.protocol == Protocols.WIREGUARD){
if (this.inbound.protocol == Protocols.WIREGUARD) {
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
} else {
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
@ -380,13 +463,12 @@
infoModal.visible = false;
},
genSubLink(subID) {
return app.subSettings.subURI+subID;
return app.subSettings.subURI + subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
return app.subSettings.subJsonURI + subID;
}
};
const infoModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-info-modal',
@ -399,20 +481,20 @@
return this.infoModal.inbound;
},
get isActive() {
if(infoModal.clientStats){
if (infoModal.clientStats) {
return infoModal.clientStats.enable;
}
return true;
},
get isEnable() {
if(infoModal.clientSettings){
if (infoModal.clientSettings) {
return infoModal.clientSettings.enable;
}
return infoModal.dbInbound.isEnable;
},
},
methods: {
copyToClipboard(elmentId,content) {
copyToClipboard(elmentId, content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
});
@ -426,11 +508,9 @@
},
getRemStats() {
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
return remained>0 ? sizeFormat(remained) : '-';
return remained > 0 ? sizeFormat(remained) : '-';
},
},
});
</script>
{{end}}

View file

@ -2,6 +2,34 @@
<html lang="en">
{{template "head" .}}
<style>
.ant-table:not(.ant-table-expanded-row .ant-table) {
outline: 1px solid #f0f0f0;
outline-offset: -1px;
border-radius: 1rem;
overflow-x: hidden;
}
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
outline-color: var(--dark-color-table-ring);
}
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
overflow-y: hidden;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
margin:-10px 22px -10px !important;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
border-bottom-left-radius: 1rem;
border-bottom-right-radius: 1rem;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
border-bottom-color: transparent;
}
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
border-bottom-left-radius: 6px;
}
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
border-bottom-right-radius: 6px;
}
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
@ -11,6 +39,9 @@
.ant-card-body {
padding: .5rem;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
margin:-10px 2px -10px !important;
}
}
.ant-col-sm-24 {
margin: 0.5rem -2rem 0.5rem 2rem;
@ -22,13 +53,14 @@
padding: 0 5px;
border-radius: 2rem;
min-width: 50px;
min-height: 22px;
}
.infinite-bar .ant-progress-inner .ant-progress-bg {
background-color: #F2EAF1;
border: #D5BED2 solid 1px;
}
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
background-color: #7a316f;
background-color: #7a316f !important;
border: #7a316f solid 1px;
}
.ant-collapse {
@ -53,6 +85,41 @@
opacity: .2;
}
}
.tr-table-box {
display: flex;
gap: 4px;
justify-content: center;
align-items: center;
}
.tr-table-rt {
flex-basis: 70px;
min-width: 70px;
text-align: end;
}
.tr-table-lt {
flex-basis: 70px;
min-width: 70px;
text-align: start;
}
.tr-table-bar {
flex-basis: 160px;
min-width: 60px;
}
.tr-infinity-ch {
font-size: 14pt;
max-height: 24px;
display: inline-flex;
align-items: center;
}
.ant-table-expanded-row .ant-table .ant-table-body {
overflow-x: hidden;
}
.ant-table-expanded-row .ant-table-tbody>tr>td {
padding: 10px 2px;
}
.ant-table-expanded-row .ant-table-thead>tr>th {
padding: 12px 2px;
}
</style>
<body>
@ -324,8 +391,8 @@
[[ sizeFormat(dbInbound.total) ]]
</template>
<template v-else>
<svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" />
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</template>
</a-tag>
@ -343,7 +410,11 @@
[[ remainedDays(dbInbound._expiryTime) ]]
</a-tag>
</a-popover>
<a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
<a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</template>
<template slot="info" slot-scope="text, dbInbound">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
@ -415,7 +486,11 @@
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
</template>
<template v-else>&infin;</template>
<template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</template>
</a-tag>
</a-popover>
</td>
@ -426,7 +501,11 @@
<a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</a-tag>
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">&infin;</a-tag>
<a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
</svg>
</a-tag>
</td>
</tr>
</table>
@ -445,7 +524,7 @@
:columns="isMobile ? innerMobileColumns : innerColumns"
:data-source="getInboundClients(record)"
:pagination=pagination(getInboundClients(record))
:style="isMobile ? 'margin: -12px 2px -13px;' : 'margin: -12px 22px -13px;'">
:style="isMobile ? 'margin: -10px 2px -11px;' : 'margin: -10px 22px -11px;'">
{{template "client_table"}}
</a-table>
</template>
@ -543,7 +622,7 @@
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 100, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
];
const innerMobileColumns = [

View file

@ -9,6 +9,9 @@
.ant-card-hoverable {
margin-inline: 0.3rem;
}
.ant-alert-error {
margin-inline: 0.3rem;
}
}
.ant-col-sm-24 {
margin-top: 10px;

View file

@ -1,14 +1,14 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/codemirror.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
@ -29,8 +29,8 @@
margin: 0;
padding: 12px .5rem;
}
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
.ant-table-thead>tr>th,
.ant-table-tbody>tr>td {
padding: 10px 0px;
}
}
@ -47,14 +47,14 @@
padding: 10px 20px;
border-bottom: 2px solid;
}
.collapse-title > i {
.collapse-title>i {
color: inherit;
font-size: 24px;
}
.ant-collapse-content-box > li {
.ant-collapse-content-box>li {
padding: 12px 0 0 0 !important;
}
.ant-list-item > li {
.ant-list-item>li {
padding: 10px 20px !important;
}
</style>
@ -443,7 +443,7 @@
<a-table :columns="outboundColumns" bordered
:row-key="r => r.key"
:data-source="outboundData"
:scroll="isMobile ? {} : { x: 200 }"
:scroll="isMobile ? {} : { x: 800 }"
:pagination="false"
:indent-size="0"
:style="isMobile ? 'padding: 5px 5px' : 'margin-right: 1px;'">

View file

@ -1,7 +1,5 @@
{{define "ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Domain Matcher'>
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
@ -13,8 +11,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Source IPs <a-icon type="question-circle"></a-icon>
</template> Source IPs <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.source"></a-input>
@ -24,8 +21,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Source Port <a-icon type="question-circle"></a-icon>
</template> Source Port <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
@ -41,7 +37,7 @@
</a-select>
</a-form-item>
<a-form-item label='Attributes'>
<a-button size="small" style="margin-left: 10px" @click="ruleModal.rule.attrs.push(['', ''])">+</a-button>
<a-button icon="plus" size="small" style="margin-left: 10px" @click="ruleModal.rule.attrs.push(['', ''])"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span: 24}">
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
@ -49,7 +45,7 @@
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)">-</a-button>
<a-button icon="minus" slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
@ -58,8 +54,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
IP <a-icon type="question-circle"></a-icon>
</template> IP <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.ip"></a-input>
@ -69,8 +64,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Domain <a-icon type="question-circle"></a-icon>
</template> Domain <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.domain"></a-input>
@ -80,8 +74,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
User <a-icon type="question-circle"></a-icon>
</template> User <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.user"></a-input>
@ -91,8 +84,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Port <a-icon type="question-circle"></a-icon>
</template> Port <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="ruleModal.rule.port"></a-input>
@ -112,19 +104,16 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
</template>
Balancer Tag <a-icon type="question-circle"></a-icon>
</template> Balancer Tag <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</table>
</a-form>
</a-modal>
<script>
const ruleModal = {
title: '',
visible: false,
@ -156,12 +145,18 @@
newRule = ruleModal.getResult();
ObjectUtil.execute(ruleModal.confirm, newRule);
},
show({ title='', okText='{{ i18n "sure" }}', rule, confirm=(rule)=>{}, isEdit=false }) {
show({
title = '',
okText = '{{ i18n "sure" }}',
rule,
confirm = (rule) => {},
isEdit = false
}) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
if (isEdit) {
this.rule.domainMatcher = rule.domainMatcher;
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
@ -197,22 +192,21 @@
this.inboundTags.push(...app.inboundTags);
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
if(app.templateSettings.reverse){
if(app.templateSettings.reverse.bridges) {
if (app.templateSettings.reverse) {
if (app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = [ "", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
}
},
close() {
ruleModal.visible = false;
ruleModal.loading(false);
},
loading(loading=true) {
loading(loading = true) {
ruleModal.confirmLoading = loading;
},
getResult() {
@ -221,34 +215,26 @@
newRule = {};
rule.type = "field";
rule.domainMatcher = value.domainMatcher;
rule.domain = value.domain.length>0 ? value.domain.split(',') : [];
rule.ip = value.ip.length>0 ? value.ip.split(',') : [];
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
rule.port = value.port;
rule.sourcePort = value.sourcePort;
rule.network = value.network;
rule.source = value.source.length>0 ? value.source.split(',') : [];
rule.user = value.user.length>0 ? value.user.split(',') : [];
rule.source = value.source.length > 0 ? value.source.split(',') : [];
rule.user = value.user.length > 0 ? value.user.split(',') : [];
rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (
value !== null &&
value !== undefined &&
!(Array.isArray(value) && value.length === 0) &&
!(typeof value === 'object' && Object.keys(value).length === 0) &&
value !== ''
) {
if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
newRule[key] = value;
}
}
return newRule;
}
};
new Vue({
delimiters: ['[[', ']]'],
el: '#rule-modal',
@ -256,6 +242,5 @@
ruleModal: ruleModal,
}
});
</script>
{{end}}

View file

@ -36,7 +36,7 @@
"status" = "Status"
"enabled" = "Enabled"
"disabled" = "Disabled"
"depleted" = "Depleted"
"depleted" = "Ended"
"depletingSoon" = "Depleting"
"offline" = "Offline"
"online" = "Online"