DESIGN REFACTOR (#600)

### New features
- New face + dark mode
  - [Change font to vazirmatn](057f3190de)
  - [use customized andtv](f956009fd2)
  - [popConfirm for del and reset client](66c98e8392)
  - [Separate page for xray config](9e1cd6315f)
  - Separate face for mobile view
- [Show online users](bf892e9965) [#559](https://github.com/alireza0/x-ui/issues/559)
- [Auto renew](96408967ae)

### Bug fixes
- [[tgbot] Retry loop on start](211c05ec29)
- [fix docker-compose version](1dcec91ce4)
- [fix redirect after restart](81d25a032c)
This commit is contained in:
Alireza Ahmadi
2023-11-09 23:31:17 +01:00
committed by GitHub
parent d4a23f8a23
commit 7c74c534f0
64 changed files with 2986 additions and 2220 deletions

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ dist/
release/ release/
/release.sh /release.sh
/x-ui /x-ui
.DS_Store

View File

@@ -47,7 +47,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.s
## Manual install & upgrade ## Manual install & upgrade
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases, generally choose Architecture `amd64` 1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases, generally choose Architecture `amd64`
2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user 2. Then upload the compressed package to the server's `/root/` directory and login to the server with user `root`
> If your server cpu architecture is not `amd64` replace another architecture > If your server cpu architecture is not `amd64` replace another architecture

View File

@@ -76,4 +76,5 @@ type Client struct {
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
TgID string `json:"tgId" form:"tgId"` TgID string `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"` SubID string `json:"subId" form:"subId"`
Reset int `json:"reset" form:"reset"`
} }

View File

@@ -1,5 +1,5 @@
--- ---
version: "3.9" version: "3"
services: services:
xui: xui:

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,6 @@
@import "../lib/style/index.less"; @import "../lib/style/index.less";
@import "../lib/style/components.less"; @import "../lib/style/components.less";
@blue-6: #0E49B5;
@border-radius-base: 1rem;
@progress-remaining-color: #EDEDED;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,28 @@ body {
overflow: hidden; overflow: hidden;
} }
body {
color: rgba(0,0,0,.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
background-color: #fff;
font-feature-settings: "tnum";
}
html {
--antd-wave-shadow-color: #0e49b5;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
::selection {
color: #0e49b5;
background-color: #0e49b530;
}
#app { #app {
height: 100%; height: 100%;
position: fixed; position: fixed;
@@ -19,6 +41,73 @@ body {
overflow: auto; overflow: auto;
} }
.ant-layout, .ant-layout * {
box-sizing: border-box;
}
.ant-spin-blur {
border-radius: 1.5rem;
}
style attribute {
text-align: center;
}
.ant-table-tbody>tr>td, .ant-table-thead>tr>th {
padding: 16px;
overflow-wrap: break-word;
}
.ant-table-thead>tr>th {
color: rgba(0,0,0,.85);
font-weight: 500;
text-align: left;
border-bottom: 1px solid #e8e8e8;
transition: background .3s ease;
}
.ant-table-row-cell-break-word {
word-wrap: break-word;
word-break: break-word;
}
.ant-table table {
width: 100%;
text-align: left;
border-radius: 1rem 1rem 0 0;
border-collapse: separate;
border-spacing: 0;
}
.ant-table {
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0,0,0,.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
font-feature-settings: "tnum";
position: relative;
clear: both;
}
.ant-card-hoverable {
cursor: auto;
cursor: pointer;
}
.ant-card {
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0,0,0,.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
font-feature-settings: "tnum";
position: relative;
background: #fff;
border-radius: 2px;
transition: all .3s;
}
.ant-space { .ant-space {
width: 100%; width: 100%;
} }
@@ -31,10 +120,22 @@ body {
.ant-layout-sider { .ant-layout-sider {
display: none; display: none;
} }
.ant-card {
margin: .5rem;
}
.ant-tabs {
margin: .5rem;
padding: .5rem;
}
} }
.ant-card { .ant-layout-content {
border-radius: 30px; min-height: auto;
}
.ant-card,
.ant-tabs {
border-radius: 1.5rem;
} }
.ant-card-hoverable { .ant-card-hoverable {
@@ -64,13 +165,78 @@ body {
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
} }
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
background-color: #04308f !important;
background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #2f67c2, rgba(123, 199, 77, 0) 100% );
background-repeat: no-repeat;
animation: ma-bg-move linear 6.6s infinite;
color: #fff;
border-radius: 0.5rem
}
@-webkit-keyframes ma-bg-move {
0% {background-position: -500px 0;}
100% {background-position: 1000px 0;}
}
@keyframes ma-bg-move {
0% {background-position: -500px 0;}
50% {background-position: 1000px 0;}
100% {background-position: 1000px 0;}
}
.ant-menu-item-active,
.ant-menu-item:hover,
.ant-menu-submenu-active,
.ant-menu-submenu-title:hover,
.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{
color:#0e49b5;
background-color: #dce9f5;
border-radius: 0.5rem;
}
.ant-menu-inline .ant-menu-item {
border-radius: 0.5rem;
}
.ant-menu-inline .ant-menu-item:after,
.ant-menu {
border-right-width: 0;
}
.ant-layout-sider-children,
.ant-pagination ul {
margin-top:-.1px;
padding:0.5rem
}
.ant-dropdown-menu,
.ant-select-dropdown-menu {
padding: .5rem;
}
.ant-dropdown-menu-item,
.ant-dropdown-menu-item:hover,
.ant-select-dropdown-menu-item,
.ant-select-dropdown-menu-item:hover,
.ant-select-dropdown-menu-item-selected,
.ant-select-selection--multiple .ant-select-selection__choice {
border-radius: .5rem;
margin-bottom: 2px;
}
@media (min-width: 769px) { @media (min-width: 769px) {
.drawer-handle { .drawer-handle {
display: none; display: none;
} }
.ant-tabs {
padding: 2rem;
}
} }
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active { .fade-in-enter,
.fade-in-leave-active,
.fade-in-linear-enter,
.fade-in-linear-leave,
.fade-in-linear-leave-active,
.fade-in-linear-enter,
.fade-in-linear-leave,
.fade-in-linear-leave-active {
opacity: 0 opacity: 0
} }
@@ -166,8 +332,7 @@ body {
} }
.ant-list-item-meta-title { .ant-list-item-meta-title {
font-weight: bold; font-size: 14px;
font-size: 16px;
} }
.ant-progress-inner { .ant-progress-inner {
@@ -181,7 +346,7 @@ body {
.ant-table-tbody>tr>td, .ant-table-tbody>tr>td,
.ant-table-thead>tr>th{ .ant-table-thead>tr>th{
padding:16px; padding:16px 5px;
} }
.ant-table-expand-icon-th, .ant-table-expand-icon-th,
@@ -190,148 +355,14 @@ body {
min-width: 30px; min-width: 30px;
} }
.ant-menu-dark, .ant-tabs {
.ant-menu-dark .ant-menu-sub,
.ant-layout-header,
.ant-layout-sider-dark,
.ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#1a212a
}
.ant-tabs:not(.ant-card-dark) {
background-color: white; background-color: white;
} }
.ant-card-dark {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
border-color:rgba(0,0,0,.09);
}
.ant-card-dark:hover {
border-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.ant-setting-textarea { .ant-setting-textarea {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.ant-card-dark-box-nohover{
padding: 0 20px 20px !important;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-box-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-card-dark .ant-table-tbody tr td,
.ant-card-dark .ant-modal-title {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-collapse-content,
.ant-card-dark .ant-calendar,
.ant-card-dark .ant-table-placeholder,
.ant-card-dark .ant-select-selection__choice,
.ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65);
background-color: #262f3d;
border: 1px solid rgb(0 150 112 / 0%);
}
.ant-card-dark .ant-list-item-meta-title,
.ant-card-dark .ant-list-item-meta-description,
.ant-card-dark .ant-form-item-label>label,
.ant-card-dark .ant-form-item,
.ant-card-dark .ant-divider-inner-text,
.ant-card-dark .ant-modal-confirm-content,
.ant-card-dark .ant-modal-confirm-title,
.ant-card-dark .ant-progress-text,
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-pagination-item a,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
.ant-card-dark .ant-empty-normal,
.ant-card-dark .ant-checkbox+span {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #11314d;
}
.ant-card-dark tbody .ant-table-expanded-row,
.ant-card-dark .ant-calendar-time-picker-inner {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
}
.ant-input-number {
min-width: 100px;
}
.ant-card-dark .ant-input,
.ant-card-dark .ant-input-number,
.ant-card-dark .ant-input-number-handler-wrap,
.ant-card-dark .ant-calendar-input,
.ant-card-dark .ant-pagination-item,
.ant-card-dark .ant-select-dropdown-menu-item-selected,
.ant-card-dark .ant-select-selection,
.ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65);
background-color: #193752;
border: 1px solid rgba(0, 65, 150, 0);
}
.ant-card-dark .ant-select-disabled .ant-select-selection {
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: #242c3a;
}
.ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-dropdown-menu-dark,
.ant-card-dark .ant-modal-content {
border: 1px solid rgba(255, 255, 255, 0.65);
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.ant-card-dark .ant-modal-content,
.ant-card-dark .ant-select-dropdown,
.ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: #1668dc;
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header { .client-table-header {
background-color: #f0f2f5; background-color: #f0f2f5;
} }
@@ -340,118 +371,362 @@ body {
background-color: #fafafa; background-color: #fafafa;
} }
.ant-card-dark .client-table-header {
background-color: #1a212a;
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .client-table-odd-row {
color: hsla(0,0%,100%,.65);
background-color: #242c3a;
}
.ant-card-dark .ant-calendar-last-month-cell .ant-calendar-date,
.ant-card-dark .ant-calendar-next-month-btn-day .ant-calendar-date {
color: hsla(0,0%,100%,.30);
}
.ant-drawer-dark {
color: hsla(0,0%,100%,.65);
}
.ant-drawer-dark .ant-drawer-wrapper-body,
.ant-drawer-dark .drawer-handle {
background-color: #1a212a;
border: 1px solid hsla(0,0%,100%,.30);
}
.ant-card-dark .ant-tag {
color: hsla(0,0%,100%,.65);
background: rgba(255,255,255,.04);
border-color: #434343;
}
.ant-card-dark .ant-tag-blue {
color: #3c9ae8;
background: #111d2c;
border-color: #15395b;
}
.ant-card-dark .ant-tag-green {
color: #6abe39;
background: #162312;
border-color: #274916;
}
.ant-card-dark .ant-tag-cyan {
color: #33bcb7;
background: #112123;
border-color: #144848;
}
.ant-card-dark .ant-tag-red {
color: #e84749;
background: #2a1215;
border-color: #58181c;
}
.ant-card-dark .ant-tag-orange {
color: #e89a3c;
background: #2b1d11;
border-color: #593815;
}
.ant-card-dark .ant-table-row-expand-icon,
.ant-card-dark .ant-checkbox-inner {
background: none;
}
.ant-card-dark .ant-switch-checked {
background-color: #0c61b0;
}
.ant-card-dark .ant-btn,
.ant-card-dark .ant-radio-button-wrapper {
color: hsla(0,0%,100%,.65);
background: none;
border: 1px solid hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc;
}
.ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65);
background-color: #073763;
border-color: #1890ff;
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
box-shadow: 0 2px 0 rgba(255,255,255,.045);
}
.ant-card-dark .ant-btn-primary:hover {
background-color: #40a9ff;
border-color: #40a9ff;
}
.ant-dark .ant-popover-content {
border: 1px solid #e8e8e8;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.ant-dark .ant-popover-inner {
background: #222a37;
}
.ant-dark .ant-popover-title,
.ant-dark .ant-popover-inner-content {
color: hsla(0,0%,100%,.65);
}
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
border-color: transparent #2e3b52 #2e3b52 transparent;
}
.ant-table-pagination.ant-pagination { .ant-table-pagination.ant-pagination {
float: left; float: left;
} }
/* change basic colors */
.ant-tag-blue {
background-color: #edf4fa;
border-color: #a9c5e7;
color: #0e49b5;
}
.ant-tag-green {
background-color: #f6ffed;
border-color: #b7eb8f;
color: #389e0d;
}
.ant-tag-purple {
background-color: #f2eaf1;
border-color: #d5bed2;
color: #7a316f;
}
.ant-tag-orange,
.ant-alert-warning {
background-color:#fff6E6;
border-color: #ffd98c;
color: #ffa031;
}
.ant-tag-red,
.ant-alert-error {
background-color:#fff0f0;
border-color: #fb9d9d;
color: #e04141;
}
.ant-input:hover,
.ant-input:focus {
background-color: #edf4fa;
}
.delete-icon:hover {
color: #E04141;
}
.normal-icon:hover {
color: #0E49B5;
}
/* DARK THEME */
.dark ::selection {
color: #fff;
background-color: #0e49b5;
}
.dark .normal-icon:hover {
color: #ffffff;
}
.dark .ant-layout-sider,
.dark .ant-drawer-content,
.ant-menu-dark,
.ant-menu-dark .ant-menu-sub,
.dark .ant-card,
.dark .ant-table,
.dark .ant-collapse-content,
.dark .ant-tabs {
background-color: #151F31;
color: #ffffffa6;
}
.dark .ant-card-hoverable:hover,
.dark .ant-space-item>.ant-tabs:hover {
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
}
.dark>.ant-layout,
.dark .drawer-handle,
.dark .ant-table-thead>tr>th,
.dark .ant-table-expanded-row,
.dark .ant-table-expanded-row:hover,
.dark .ant-table-expanded-row .ant-table-tbody,
.dark .ant-calendar {
background-color: #101828;
color: rgb(255 255 255 /65%);
}
.dark .ant-table-expanded-row .ant-table-thead>tr:first-child>th {
border-radius: 0;
}
.dark .ant-calendar,
.dark .ant-card-bordered {
border-color: #151f31;
}
.dark .ant-table-tbody>tr>td,
.dark .ant-table-thead>tr>th,
.dark .ant-card-head,
.dark .ant-modal-header,
.dark .ant-collapse>.ant-collapse-item,
.dark .ant-tabs-bar,
.dark .ant-list-split .ant-list-item,
.dark .ant-popover-title,
.dark .ant-calendar-header,
.dark .ant-calendar-input-wrap {
border-bottom-color: #2C3950;
}
.dark .ant-modal-footer,
.dark .ant-collapse-content,
.dark .ant-calendar-footer,
.dark .ant-divider-horizontal.ant-divider-with-text-center:before,
.dark .ant-divider-horizontal.ant-divider-with-text-center:after {
border-top-color: #2c3950;
}
.dark .ant-progress-text,
.dark .ant-card-head,
.dark .ant-form,
.dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
.dark .ant-form-item i,
.dark .ant-modal-close-x,
.dark .ant-pagination-item a,
.dark li:not(.ant-pagination-disabled) i,
.dark .ant-form .anticon,
.dark .ant-tabs-tab-arrow-show:not(.ant-tabs-tab-btn-disabled),
.dark .anticon-close,
.dark .ant-list-item-meta-title,
.dark .ant-list-item-meta-description,
.dark .ant-select-selection i,
.dark .ant-modal-confirm-title,
.dark .ant-modal-confirm-content,
.dark .ant-popover-message,
.dark .ant-modal,
.dark .ant-divider-inner-text,
.dark .ant-popover-title,
.dark .ant-popover-inner-content,
.dark h2 {
color: rgb(255 255 255 / 65%);
}
.dark .ant-pagination-disabled i,
.dark .ant-tabs-tab-btn-disabled {
color: rgb(255 255 255 / 25%);
}
.dark .ant-input,
.dark .ant-input-group-addon,
.dark .ant-collapse,
.dark .ant-select-selection,
.dark .ant-input-number,
.dark .ant-input-number-handler-wrap,
.dark .ant-pagination-item-active,
.dark .ant-table-placeholder,
.dark .ant-empty-normal,
.dark.ant-select-dropdown,
.dark .ant-select-dropdown,
.dark .ant-select-dropdown-menu-item,
.dark .ant-divider:not(.ant-divider-with-text-center),
.dark .ant-calendar-input,
.dark .ant-calendar-time-picker-inner {
background-color: #222D42;
border-color: #2c3950;
color: rgb(255 255 255 / 65%);
}
.dark .ant-select-selection:hover,
.dark .ant-calendar-picker-clear,
.dark .ant-input-number:hover,
.dark .ant-input-number:focus,
.dark .ant-input:hover,
.dark .ant-input:focus {
background-color: rgb(14 73 181 / 30%);
border-color: #0E49B5;
}
.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) {
color: rgb(255 255 255 / 65%);
background-color: rgb(14 73 181 / 30%);
border: 1px solid #0e49b5;
}
.dark .ant-radio-button-wrapper,
.dark .ant-radio-button-wrapper:before {
color: rgb(255 255 255 / 65%);
background-color: rgb(14 73 181 / 30%);
border: 1px solid #0e49b5;
border-left: inherit;
}
.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger) ,
.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger) {
color: #ffffff;
background-color: rgb(14 73 181 / 50%);
border-color: #0e49b5;
}
.dark .ant-btn-primary[disabled],
.dark .ant-calendar-ok-btn-disabled {
color: rgb(255 255 255 / 35%);
background-color: #2c3950;
border-color: #42516c;
}
.dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td {
background-color: #122444;
}
.dark .ant-table-row-expand-icon {
color: #fff;
background-color: #fff0;
border-color: #9ea2a8;
}
.dark .ant-table-row-expand-icon:hover {
color: #0e49b5;
background-color: #fff0;
border-color: #0e49b5;
}
.dark .ant-switch:not(.ant-switch-checked) {
background-color: #2C3950;
}
.dark .ant-progress-line .ant-progress-inner {
background-color: #2c3950;
}
.dark .ant-progress-circle-trail {
stroke: #2c3950 !important;
}
.ant-dropdown-menu-dark,
.dark .ant-popover-inner {
background-color: #222D42;
}
.dark>.ant-popover-content>.ant-popover-arrow {
border-color: #222D42;
}
.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,
.dark .ant-select-dropdown-menu-item-selected,
.dark .ant-select-dropdown-menu-item:hover,
.dark .ant-calendar-time-picker-select-option-selected {
background-color: #313f5a;
}
.ant-menu-dark .ant-menu-item:hover {
background-color: #2c3950;
}
.dark .ant-alert-message {
color: rgb(255 255 255 /85%);
}
.dark .ant-tag {
color: rgb(255 255 255 / 65%);
background-color: #ffffff0a;
border-color: #344461;
}
.dark .ant-tag-blue {
background-color: #111a2c;
border-color: #0f367e;
color: #3c89e8;
}
.dark .ant-tag-red,
.dark .ant-alert-error {
background-color: #291515;
border-color: #5C2626;
color: #e04141;
}
.dark .ant-tag-orange,
.dark .ant-alert-warning {
background-color: #312313;
border-color: #593914;
color: #ffa031;
}
.dark .ant-tag-green {
background-color: #142429;
border-color: #23432c;
color: #61bf39;
}
.dark .ant-tag-purple {
background-color: #2c1e32;
border-color: #49394e;
color: #f2eaf1;
}
.dark .ant-modal-content,
.dark .ant-modal-header {
background-color: #181f2c;
}
.dark .ant-modal-title,
.dark .ant-form-item-label>label,
.dark .ant-checkbox-wrapper,
.dark .ant-form-item,
.dark .ant-calendar-footer .ant-calendar-today-btn,
.dark .ant-calendar-footer .ant-calendar-time-picker-btn,
.dark .ant-calendar-day-select,
.dark .ant-calendar-month-select,
.dark .ant-calendar-year-select,
.dark .ant-calendar-date {
color: rgb(255 255 255 / 65%);
}
.dark .ant-calendar-next-month-btn-day .ant-calendar-date,
.dark .ant-calendar-last-month-cell .ant-calendar-date {
color: #2c3950;
}
.dark .ant-calendar-selected-day .ant-calendar-date {
background-color: #0e49b5 !important;
color: #fff;
}
.dark .ant-calendar-date:hover,
.dark .ant-calendar-time-picker-select li:hover {
background-color: #313f5a;
color: #fff;
}
.dark .ant-calendar-header a:hover,
.dark .ant-calendar-header a:hover::before,
.dark .ant-calendar-header a:hover::after {
border-color: #fff;
}
.dark .ant-calendar-time-picker-select li:focus {
color: #ffffff;
font-weight: 600;
outline: none;
background-color: #0e49b5;
}
.dark .ant-calendar-time-picker-select {
border-right-color: #2C3950;
}
.dark .anticon-close-circle {
color: #E04141;
}
.dark .ant-spin-nested-loading>div>.ant-spin .ant-spin-text {
text-shadow: 0 1px 2px #00000077;
}
.dark .ant-spin {
color: #ffffff;
}
.dark .ant-spin-dot-item {
background-color: #ffffffff;
}

View File

@@ -139,6 +139,19 @@ class DBInbound {
return Inbound.fromJson(config); return Inbound.fromJson(config);
} }
isMultiUser() {
switch (this.protocol) {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
return true;
case Protocols.SHADOWSOCKS:
return this.toInbound().isSSMultiUser;
default:
return false;
}
}
hasLink() { hasLink() {
switch (this.protocol) { switch (this.protocol) {
case Protocols.VMESS: case Protocols.VMESS:
@@ -183,7 +196,6 @@ class AllSetting {
this.tgBotLoginNotify = false; this.tgBotLoginNotify = false;
this.tgCpu = ""; this.tgCpu = "";
this.tgLang = ""; this.tgLang = "";
this.xrayTemplateConfig = "";
this.subEnable = false; this.subEnable = false;
this.subListen = ""; this.subListen = "";
this.subPort = "2096"; this.subPort = "2096";

View File

@@ -1577,7 +1577,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
} }
}; };
Inbound.VmessSettings.Vmess = class extends XrayCommonClass { Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) { constructor(id=RandomUtil.randomUUID(), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super(); super();
this.id = id; this.id = id;
this.email = email; this.email = email;
@@ -1586,6 +1586,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.reset = reset;
} }
static fromJson(json={}) { static fromJson(json={}) {
@@ -1597,6 +1598,7 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.reset,
); );
} }
get _expiryTime() { get _expiryTime() {
@@ -1665,7 +1667,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) { constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super(); super();
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
@@ -1675,6 +1677,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.reset = reset;
} }
static fromJson(json={}) { static fromJson(json={}) {
@@ -1687,6 +1690,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.reset,
); );
} }
@@ -1786,7 +1790,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
} }
}; };
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) { constructor(password=RandomUtil.randomSeq(10), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super(); super();
this.password = password; this.password = password;
this.email = email; this.email = email;
@@ -1795,6 +1799,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.reset = reset;
} }
toJson() { toJson() {
@@ -1806,6 +1811,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
enable: this.enable, enable: this.enable,
tgId: this.tgId, tgId: this.tgId,
subId: this.subId, subId: this.subId,
reset: this.reset,
}; };
} }
@@ -1818,6 +1824,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.reset,
); );
} }
@@ -1922,7 +1929,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
}; };
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) { constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(9), totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16), reset=0) {
super(); super();
this.method = method; this.method = method;
this.password = password; this.password = password;
@@ -1932,6 +1939,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.reset = reset;
} }
toJson() { toJson() {
@@ -1944,6 +1952,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
enable: this.enable, enable: this.enable,
tgId: this.tgId, tgId: this.tgId,
subId: this.subId, subId: this.subId,
reset: this.reset,
}; };
} }
@@ -1957,6 +1966,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.reset,
); );
} }

View File

@@ -33,13 +33,15 @@ function safeBase64(str) {
function formatSecond(second) { function formatSecond(second) {
if (second < 60) { if (second < 60) {
return second.toFixed(0) + ' s'; return second.toFixed(0) + 's';
} else if (second < 3600) { } else if (second < 3600) {
return (second / 60).toFixed(0) + ' m'; return (second / 60).toFixed(0) + 'm';
} else if (second < 3600 * 24) { } else if (second < 3600 * 24) {
return (second / 3600).toFixed(0) + ' h'; return (second / 3600).toFixed(0) + 'h';
} else { } else {
return (second / 3600 / 24).toFixed(0) + ' d'; day = Math.floor(second / 3600 / 24);
remain = ((second/3600) - (day*24)).toFixed(0);
return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
} }
} }
@@ -53,7 +55,7 @@ function addZero(num) {
function toFixed(num, n) { function toFixed(num, n) {
n = Math.pow(10, n); n = Math.pow(10, n);
return Math.round(num * n) / n; return Math.floor(num * n) / n;
} }
function debounce(fn, delay) { function debounce(fn, delay) {
@@ -94,11 +96,13 @@ function setCookie(cname, cvalue, exdays) {
function usageColor(data, threshold, total) { function usageColor(data, threshold, total) {
switch (true) { switch (true) {
case data === null: case data === null:
return "green";
case total < 0:
return "blue"; return "blue";
case total <= 0: case total == 0:
return "blue"; return "purple";
case data < total - threshold: case data < total - threshold:
return "cyan"; return "blue";
case data < total: case data < total:
return "orange"; return "orange";
default: default:
@@ -106,6 +110,28 @@ function usageColor(data, threshold, total) {
} }
} }
function userExpiryColor(threshold, client, isDark = false) {
if (!client.enable) {
return isDark ? '#2c3950' : '#bcbcbc';
}
now = new Date().getTime(),
expiry = client.expiryTime;
switch (true) {
case expiry === null:
return "#389e0d";
case expiry < 0:
return "#0e49b5";
case expiry == 0:
return "#7a316f";
case now < expiry - threshold:
return "#0e49b5";
case now < expiry:
return "#ffa031";
default:
return "#e04141";
}
}
function doAllItemsExist(array1, array2) { function doAllItemsExist(array1, array2) {
for (let i = 0; i < array1.length; i++) { for (let i = 0; i < array1.length; i++) {
if (!array2.includes(array1[i])) { if (!array2.includes(array1[i])) {

View File

@@ -35,7 +35,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.POST("/onlines", a.onlines)
} }
func (a *InboundController) getInbounds(c *gin.Context) { func (a *InboundController) getInbounds(c *gin.Context) {
@@ -253,3 +253,7 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
} }
jsonMsg(c, "All delpeted clients are deleted", nil) jsonMsg(c, "All delpeted clients are deleted", nil)
} }
func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClinets(), nil)
}

View File

@@ -0,0 +1,50 @@
package controller
import (
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type XraySettingController struct {
XraySettingService service.XraySettingService
SettingService service.SettingService
}
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
a := &XraySettingController{}
a.initRouter(g)
return a
}
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xray")
g.POST("/", a.getXraySetting)
g.POST("/update", a.updateSetting)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
}
func (a *XraySettingController) getXraySetting(c *gin.Context) {
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, xraySetting, nil)
}
func (a *XraySettingController) updateSetting(c *gin.Context) {
xraySetting := c.PostForm("xraySetting")
err := a.XraySettingService.SaveXraySetting(xraySetting)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}

View File

@@ -7,8 +7,9 @@ import (
type XUIController struct { type XUIController struct {
BaseController BaseController
inboundController *InboundController inboundController *InboundController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController
} }
func NewXUIController(g *gin.RouterGroup) *XUIController { func NewXUIController(g *gin.RouterGroup) *XUIController {
@@ -24,9 +25,11 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.GET("/inbounds", a.inbounds) g.GET("/inbounds", a.inbounds)
g.GET("/settings", a.settings) g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g)
} }
func (a *XUIController) index(c *gin.Context) { func (a *XUIController) index(c *gin.Context) {
@@ -40,3 +43,7 @@ func (a *XUIController) inbounds(c *gin.Context) {
func (a *XUIController) settings(c *gin.Context) { func (a *XUIController) settings(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil) html(c, "settings.html", "pages.settings.title", nil)
} }
func (a *XUIController) xraySettings(c *gin.Context) {
html(c, "xray.html", "pages.xray.title", nil)
}

View File

@@ -2,12 +2,10 @@ package entity
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"net" "net"
"strings" "strings"
"time" "time"
"x-ui/util/common" "x-ui/util/common"
"x-ui/xray"
) )
type Msg struct { type Msg struct {
@@ -17,36 +15,35 @@ type Msg struct {
} }
type AllSetting struct { type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"` WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"` WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"` WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"` WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"` WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
PageSize int `json:"pageSize" form:"pageSize"` PageSize int `json:"pageSize" form:"pageSize"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"` ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"`
TgCpu int `json:"tgCpu" form:"tgCpu"` TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"` TgLang string `json:"tgLang" form:"tgLang"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` TimeLocation string `json:"timeLocation" form:"timeLocation"`
TimeLocation string `json:"timeLocation" form:"timeLocation"` SubEnable bool `json:"subEnable" form:"subEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"` SubListen string `json:"subListen" form:"subListen"`
SubListen string `json:"subListen" form:"subListen"` SubPort int `json:"subPort" form:"subPort"`
SubPort int `json:"subPort" form:"subPort"` SubPath string `json:"subPath" form:"subPath"`
SubPath string `json:"subPath" form:"subPath"` SubDomain string `json:"subDomain" form:"subDomain"`
SubDomain string `json:"subDomain" form:"subDomain"` SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubCertFile string `json:"subCertFile" form:"subCertFile"` SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` SubUpdates int `json:"subUpdates" form:"subUpdates"`
SubUpdates int `json:"subUpdates" form:"subUpdates"` SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
} }
func (s *AllSetting) CheckValid() error { func (s *AllSetting) CheckValid() error {
@@ -97,13 +94,7 @@ func (s *AllSetting) CheckValid() error {
s.WebBasePath += "/" s.WebBasePath += "/"
} }
xrayConfig := &xray.Config{} _, err := time.LoadLocation(s.TimeLocation)
err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig)
if err != nil {
return common.NewError("xray template config invalid:", err)
}
_, err = time.LoadLocation(s.TimeLocation)
if err != nil { if err != nil {
return common.NewError("time location not exist:", s.TimeLocation) return common.NewError("time location not exist:", s.TimeLocation)
} }

View File

@@ -13,6 +13,20 @@
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
/* vazirmatn-regular - arabic_latin_latin-ext */
@font-face {
font-display: swap;
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 400;
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
}
</style> </style>
<title>{{ .host }}-{{ i18n .title}}</title> <title>{{ .host }}-{{ i18n .title}}</title>
</head> </head>

View File

@@ -1,8 +1,7 @@
{{define "promptModal"}} {{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false" :closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="themeSwitcher.darkCardClass" :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type" <a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value" v-model="promptModal.value"
:autosize="{minRows: 10, maxRows: 20}" :autosize="{minRows: 10, maxRows: 20}"

View File

@@ -1,9 +1,8 @@
{{define "qrcodeModal"}} {{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" :closable="true"
:class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="300px"> width="300px" :class="themeSwitcher.currentTheme">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag> <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<template v-if="app.subSettings.enable && qrModal.subId"> <template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>Subscription</a-divider> <a-divider>Subscription</a-divider>
@@ -11,7 +10,7 @@
</template> </template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes"> <template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag> <a-tag color="blue" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas> <canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
</template> </template>
</a-modal> </a-modal>

View File

@@ -1,8 +1,7 @@
{{define "textModal"}} {{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.darkCardClass" :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}" :class="themeSwitcher.currentTheme">
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName"> :download="txtModal.fileName">

View File

@@ -2,11 +2,6 @@
<html lang="en"> <html lang="en">
{{template "head" .}} {{template "head" .}}
<style> <style>
#app {
padding-top: 100px;
}
h1 { h1 {
text-align: center; text-align: center;
margin: 20px 0 50px 0; margin: 20px 0 50px 0;
@@ -43,23 +38,88 @@
font-weight: bold; font-weight: bold;
} }
#app {
overflow: hidden;
}
#login {
animation: charge .5s both;
background-color: #fff;
border-radius: 2rem;
padding: 3rem;
}
#login:hover {
box-shadow: 0 2px 8px rgba(0,0,0,.09);
}
@keyframes charge {
from {transform: translateY(5rem);opacity: 0}
to {transform: translateY(0);opacity: 1}
}
@keyframes wave {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
.wave {
opacity: .6;
position: absolute;
bottom: 40%;
left: 50%;
width: 6000px;
height: 6000px;
background: #000;
margin-left: -3000px;
transform-origin: 50% 48%;
border-radius: 46%;
animation: wave 72s infinite linear;
pointer-events: none;
}
.wave2 {
animation: wave 88s infinite linear;
opacity: .3;
}
.wave3 {
animation: wave 80s infinite linear;
opacity: .1;
}
.wave {
background: #0e49b515;
}
.under {
background-color: #dce9f5;
}
.dark .wave {
background: rgb(14 73 181 / 20%);
}
.dark .under {
background-color: #101828;
}
.dark #login {
background-color: #151F31;
}
.dark h1 {
color: rgb(255 255 255 / 85%);
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak :class="themeSwitcher.darkCardClass"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content> <a-layout-content class="under">
<div class='wave'></div>
<div class='wave wave2'></div>
<div class='wave wave3'></div>
<a-row type="flex" justify="center" align="middle" style="height: 100%;">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="6" id="login">
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> <a-col>
<h1 class="title" :style="themeSwitcher.textStyle">{{ i18n "pages.login.title" }}</h1> <h1 class="title">{{ i18n "pages.login.title" }}</h1>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> <a-col span="24">
<a-form> <a-form>
<a-form-item> <a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}' <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus> @keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle"/> <a-icon slot="prefix" type="user" style="font-size: 16px;"/>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -77,8 +137,8 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered"> <a-row justify="center" class="centered">
<a-col :span="12"> <a-col :span="24">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select ref="selectLang" v-model="lang" @change="setLang(lang)" style="width: 150px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs"> <a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
@@ -89,12 +149,19 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered"> <a-row justify="center" class="centered">
<theme-switch /> <a-col>
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>&nbsp;
</a-col>
<a-col>
<theme-switch />
</a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-col> </a-col>
</a-row> </a-row>
</a-col>
</a-row>
</a-layout-content> </a-layout-content>
</transition> </transition>
</a-layout> </a-layout>

View File

@@ -1,16 +1,14 @@
{{define "clientsBulkModal"}} {{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok" <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass" :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline"> <a-form layout="inline">
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td>{{ i18n "pages.client.method" }}</td> <td>{{ i18n "pages.client.method" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px" <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="0">Random</a-select-option> <a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option> <a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option> <a-select-option :value="2">Random+Prefix+Num</a-select-option>
@@ -64,7 +62,7 @@
<td>Flow</td> <td>Flow</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -132,11 +130,27 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker> v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
<tr v-if="clientsBulkModal.expiryTime != 0">
<td>
<span>{{ i18n "pages.client.renew" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.renewDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
</table> </table>
</a-form> </a-form>
</a-modal> </a-modal>
@@ -162,6 +176,7 @@
tgId: "", tgId: "",
flow: "", flow: "",
delayedStart: false, delayedStart: false,
reset: 0,
ok() { ok() {
clients = []; clients = [];
method=clientsBulkModal.emailMethod; method=clientsBulkModal.emailMethod;
@@ -186,6 +201,7 @@
if(clientsBulkModal.inbound.canEnableTlsFlow()){ if(clientsBulkModal.inbound.canEnableTlsFlow()){
newClient.flow = clientsBulkModal.flow; newClient.flow = clientsBulkModal.flow;
} }
newClient.reset = clientsBulkModal.reset;
clients.push(newClient); clients.push(newClient);
} }
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id); ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
@@ -209,6 +225,7 @@
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.delayedStart = false; this.delayedStart = false;
this.reset = 0;
}, },
newClient(protocol) { newClient(protocol) {
switch (protocol) { switch (protocol) {

View File

@@ -1,8 +1,10 @@
{{define "clientsModal"}} {{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok" <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass" :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'> <template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
{{template "form/client"}} {{template "form/client"}}
</a-modal> </a-modal>
<script> <script>
@@ -111,6 +113,12 @@
get statsColor() { get statsColor() {
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB); return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
}, },
get delayedStart() {
return this.clientModal.delayedStart;
},
set delayedStart(value) {
this.clientModal.delayedStart = value;
},
get delayedExpireDays() { get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0; return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
}, },
@@ -123,7 +131,7 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.darkCardClass, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {

View File

@@ -11,6 +11,10 @@
<a-icon type="setting"></a-icon> <a-icon type="setting"></a-icon>
<span>{{ i18n "menu.settings"}}</span> <span>{{ i18n "menu.settings"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}xui/xray">
<a-icon type="tool"></a-icon>
<span>{{ i18n "menu.xray"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}logout"> <a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon> <a-icon type="logout"></a-icon>
<span>{{ i18n "menu.logout"}}</span> <span>{{ i18n "menu.logout"}}</span>
@@ -22,7 +26,7 @@
<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" collapsed-width="0">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys=""> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<theme-switch /> <theme-switch />
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
@@ -31,17 +35,16 @@
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-layout-sider> </a-layout-sider>
<a-drawer id="sider-drawer" placement="left" :closable="false" <a-drawer id="sider-drawer" placement="left" :closable="false" :class="themeSwitcher.currentTheme"
@close="siderDrawer.close()" @close="siderDrawer.close()"
:visible="siderDrawer.visible" :visible="siderDrawer.visible"
:wrap-class-name="themeSwitcher.darkDrawerClass"
:wrap-style="{ padding: 0 }"> :wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle"> <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon> <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div> </div>
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys=""> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
<theme-switch /> <theme-switch />
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>

View File

@@ -4,12 +4,12 @@
:placeholder="placeholder" :placeholder="placeholder"
@input="$emit('input', $event.target.value)"> @input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix> <template v-if="icon" #prefix>
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" /> <a-icon :type="icon" style="font-size: 16px;" />
</template> </template>
<template #addonAfter> <template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'" <a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword" @click="toggleShowPassword"
:style="'font-size: 16px;' + themeSwitcher.textStyle" /> style="font-size: 16px;" />
</template> </template>
</a-input> </a-input>
</template> </template>

View File

@@ -1,8 +1,6 @@
{{define "component/themeSwitchTemplate"}} {{define "component/themeSwitchTemplate"}}
<template> <template>
<a-switch :default-checked="themeSwitcher.isDarkTheme" <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="themeSwitcher.toggleTheme()"> @change="themeSwitcher.toggleTheme()">
</a-switch> </a-switch>
</template> </template>
@@ -10,39 +8,17 @@
{{define "component/themeSwitcher"}} {{define "component/themeSwitcher"}}
<script> <script>
const colors = {
dark: {
bg: "#242c3a",
text: "hsla(0,0%,100%,.65)"
},
light: {
bg: '#f0f2f5',
text: "rgba(0, 0, 0, 0.7)",
}
}
function createThemeSwitcher() { function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true'; const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const theme = isDarkTheme ? 'dark' : 'light'; const theme = isDarkTheme ? 'dark' : 'light';
return { return {
isDarkTheme, isDarkTheme,
bgStyle: `background: ${colors[theme].bg};`,
textStyle: `color: ${colors[theme].text};`,
darkClass: isDarkTheme ? 'ant-dark' : '',
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
get currentTheme() { get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light'; return this.isDarkTheme ? 'dark' : 'light';
}, },
toggleTheme() { toggleTheme() {
this.isDarkTheme = !this.isDarkTheme; this.isDarkTheme = !this.isDarkTheme;
this.theme = this.isDarkTheme ? 'dark' : 'light';
localStorage.setItem('dark-mode', this.isDarkTheme); localStorage.setItem('dark-mode', this.isDarkTheme);
this.bgStyle = `background: ${colors[this.theme].bg};`;
this.textStyle = `color: ${colors[this.theme].text};`;
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
}, },
}; };
} }

View File

@@ -1,8 +1,5 @@
{{define "form/client"}} {{define "form/client"}}
<a-form layout="inline" v-if="client"> <a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td>{{ i18n "pages.inbounds.enable" }}</td> <td>{{ i18n "pages.inbounds.enable" }}</td>
@@ -66,7 +63,7 @@
<td>Flow</td> <td>Flow</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -107,11 +104,11 @@
<td>{{ i18n "pages.client.delayedStart" }}</td> <td>{{ i18n "pages.client.delayedStart" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch> <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
<tr v-if="clientModal.delayedStart"> <tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td> <td>{{ i18n "pages.client.expireDays" }}</td>
<td> <td>
<a-form-item> <a-form-item>
@@ -132,9 +129,25 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="client._expiryTime" style="width: 250px;"></a-date-picker> v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag> <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item>
</td>
</tr>
<tr v-if="client.expiryTime != 0">
<td>
<span>{{ i18n "pages.client.renew" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.renewDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model.number="client.reset" :min="0"></a-input-number>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>

View File

@@ -22,7 +22,7 @@
<td>{{ i18n "protocol" }}</td> <td>{{ i18n "protocol" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -80,7 +80,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker> v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>

View File

@@ -21,7 +21,7 @@
<td>{{ i18n "pages.inbounds.network"}}</td> <td>{{ i18n "pages.inbounds.network"}}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">tcp+udp</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option> <a-select-option value="udp">udp</a-select-option>

View File

@@ -1,19 +1,21 @@
{{define "form/http"}} {{define "form/http"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <table style="width: 100%; text-align: center; margin-bottom: 10px;">
<a-row> <tr>
<a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button> <td width="45%">{{ i18n "username" }}</td>
</a-row> <td width="45%">{{ i18n "password" }}</td>
<a-input-group v-for="(account, index) in inbound.settings.accounts"> <td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())">+</a-button></td>
<a-input style="width: 45%" v-model.trim="account.user" </tr>
addon-before='{{ i18n "username" }}'></a-input> </table>
<a-input style="width: 55%" v-model.trim="account.pass" <a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
addon-before='{{ i18n "password" }}'> <a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button> <a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -3,100 +3,7 @@
<template v-if="inbound.isSSMultiUser"> <template v-if="inbound.isSSMultiUser">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> {{template "form/client"}}
<tr>
<td>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>password
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.tgBotEnable">
<td>Telegram Username</td>
<td>
<a-form-item>
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else> <a-collapse v-else>
@@ -119,7 +26,7 @@
<td>{{ i18n "encryption" }}</td> <td>{{ i18n "encryption" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange"> <a-select v-model="inbound.settings.method" style="width: 250px;" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -139,7 +46,7 @@
<td>{{ i18n "pages.inbounds.network" }}</td> <td>{{ i18n "pages.inbounds.network" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">tcp+udp</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option> <a-select-option value="udp">udp</a-select-option>

View File

@@ -2,7 +2,7 @@
<a-form layout="inline"> <a-form layout="inline">
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td>{{ i18n "password" }}</td> <td style="width: 30%;">{{ i18n "password" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-switch :checked="inbound.settings.auth === 'password'" <a-switch :checked="inbound.settings.auth === 'password'"
@@ -12,21 +12,23 @@
</tr> </tr>
<tr v-if="inbound.settings.auth === 'password'"> <tr v-if="inbound.settings.auth === 'password'">
<td colspan="2"> <td colspan="2">
<a-form-item> <table style="width: 100%; text-align: center; margin-bottom: 10px;">
<a-row> <tr>
<a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button> <td width="45%">{{ i18n "username" }}</td>
</a-row> <td width="45%">{{ i18n "password" }}</td>
<a-input-group v-for="(account, index) in inbound.settings.accounts"> <td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
<a-input style="width: 45%" v-model.trim="account.user" </tr>
addon-before='{{ i18n "username" }}'></a-input> </table>
<a-input style="width: 55%" v-model.trim="account.pass" <a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
addon-before='{{ i18n "password" }}'> <a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<template slot="addonAfter"> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button> </a-input>
</template> <a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
</a-input> <template slot="addonAfter">
</a-input-group> <a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
</a-form-item> </template>
</a-input>
</a-input-group>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -2,98 +2,7 @@
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> {{template "form/client"}}
<tr>
<td>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>Password</td>
<td>
<a-form-item>
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.tgBotEnable">
<td>Telegram Username</td>
<td>
<a-form-item>
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else> <a-collapse v-else>
@@ -124,7 +33,7 @@
<!-- trojan fallbacks --> <!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider> <a-divider style="margin:0;">
fallback[[ index + 1 ]] fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)" <a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/> style="color: rgb(255, 77, 79);cursor: pointer;"/>
@@ -144,7 +53,7 @@
<a-form-item label="xver"> <a-form-item label="xver">
<a-input-number v-model.number="fallback.xver"></a-input-number> <a-input-number v-model.number="fallback.xver"></a-input-number>
</a-form-item> </a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form> </a-form>
<a-divider style="margin:0;"></a-divider>
</template> </template>
{{end}} {{end}}

View File

@@ -2,109 +2,7 @@
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> {{template "form/client"}}
<tr>
<td>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="client.email = RandomUtil.randomLowerAndNum(9)" type="sync"> </a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="inbound.canEnableTlsFlow()">
<td>Flow</td>
<td>
<a-form-item>
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.tgBotEnable">
<td>Telegram Username</td>
<td>
<a-form-item>
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else> <a-collapse v-else>
@@ -137,7 +35,7 @@
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider> <a-divider style="margin:0;">
fallback[[ index + 1 ]] fallback[[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)" <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/> style="color: rgb(255, 77, 79);cursor: pointer;"/>
@@ -157,7 +55,7 @@
<a-form-item label="xver"> <a-form-item label="xver">
<a-input-number v-model.number="fallback.xver"></a-input-number> <a-input-number v-model.number="fallback.xver"></a-input-number>
</a-form-item> </a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form> </a-form>
<a-divider style="margin:0;"></a-divider>
</template> </template>
{{end}} {{end}}

View File

@@ -2,98 +2,7 @@
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> {{template "form/client"}}
<tr>
<td>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.subSettings.enable">
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
<td>
<a-form-item>
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr v-if="client.email && app.tgBotEnable">
<td>Telegram Username</td>
<td>
<a-form-item>
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
</a-form-item>
</td>
</tr>
<tr>
<td>
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr>
<td>{{ i18n "pages.client.delayedStart" }}</td>
<td>
<a-form-item>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
</td>
</tr>
<tr v-if="delayedStart">
<td>{{ i18n "pages.client.expireDays" }}</td>
<td>
<a-form-item>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
</td>
</tr>
<tr v-else>
<td>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</td>
<td>
<a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item>
</td>
</tr>
</table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else> <a-collapse v-else>

View File

@@ -5,7 +5,7 @@
<td>{{ i18n "camouflage" }}</td> <td>{{ i18n "camouflage" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option> <a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option> <a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option> <a-select-option value="utp">utp (BT download)</a-select-option>

View File

@@ -5,7 +5,7 @@
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td> <td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.quic.security" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none</a-select-option> <a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option> <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option> <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -17,7 +17,7 @@
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="inbound.stream.quic.key" style="width: 200px;"></a-input> <a-input v-model.trim="inbound.stream.quic.key" style="width: 250px;"></a-input>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
@@ -25,7 +25,7 @@
<td>{{ i18n "camouflage" }}</td> <td>{{ i18n "camouflage" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.quic.type" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.quic.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">none (not camouflage)</a-select-option> <a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option> <a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option> <a-select-option value="utp">utp (BT download)</a-select-option>

View File

@@ -1,10 +1,9 @@
{{define "form/streamSettings"}} {{define "form/streamSettings"}}
<!-- select stream network --> <!-- select stream network -->
<a-divider style="margin:0;"></a-divider>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="{{ i18n "transmission" }}"> <a-form-item label="{{ i18n "transmission" }}">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" <a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.darkCardClass" style="width: 150px;"> style="width: 150px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option> <a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option> <a-select-option value="ws">WebSocket</a-select-option>

View File

@@ -33,8 +33,7 @@
<td>T-Proxy</td> <td>T-Proxy</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;" <a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="off">OFF</a-select-option> <a-select-option value="off">OFF</a-select-option>
<a-select-option value="redirect">Redirect</a-select-option> <a-select-option value="redirect">Redirect</a-select-option>
<a-select-option value="tproxy">T-Proxy</a-select-option> <a-select-option value="tproxy">T-Proxy</a-select-option>

View File

@@ -42,25 +42,16 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2" width="100%">
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'> <a-form-item>
<a-row> <span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button size="small" <a-button size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('', '')">+</a-button>
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')"> <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-button> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-row> </a-input>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers"> <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.name" <a-button slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
</template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
@@ -92,24 +83,19 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2" width="100%">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'> <a-form-item>
<a-row> <span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
<a-button size="small" <a-button size="small" style="margin-left: 10px"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"> @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
+ <a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
</a-button> <a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
</a-row> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers"> </a-input>
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'> placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
@click="inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
</template> </template>
</a-input> </a-input>
</a-input-group> </a-input-group>

View File

@@ -1,47 +1,24 @@
{{define "form/streamWS"}} {{define "form/streamWS"}}
<a-form layout="inline"> <a-form layout="inline">
<table width="100%" class="ant-table-tbody"> <a-form-item label="AcceptProxyProtocol">
<tr v-if="inbound.canEnableTls()"> <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
<td>Accept Proxy Protocol</td> </a-form-item>
<td> <br>
<a-form-item> <a-form-item label='{{ i18n "path" }}'>
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch> <a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>
</td> <br>
</tr> <a-form-item style="width: 100%;">
<tr> <span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<td>{{ i18n "path" }}</td> <a-button size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader()">+</a-button>
<td> <a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
<a-form-item> <a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<a-input v-model.trim="inbound.stream.ws.path" style="width: 250px;"></a-input> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-form-item> </a-input>
</td> <a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
</tr> <a-button slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
<tr> </a-input>
<td colspan="2"> </a-input-group>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'> </a-form-item>
<a-row>
<a-button size="small"
@click="inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.ws.removeHeader(index)">
-
</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</td>
</tr>
</table>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/tlsSettings"}} {{define "form/tlsSettings"}}
<!-- tls enable --> <!-- tls enable -->
<a-divider style="margin:0;"></a-divider>
<a-form v-if="inbound.canSetTls()" layout="inline"> <a-form v-if="inbound.canSetTls()" layout="inline">
<a-divider style="margin:0;"></a-divider>
<a-form-item label="TLS"> <a-form-item label="TLS">
<a-switch v-model="inbound.tls"> <a-switch v-model="inbound.tls">
</a-switch> </a-switch>
@@ -26,30 +26,25 @@
<td>CipherSuites</td> <td>CipherSuites</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">auto</a-select-option> <a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>MinVersion</td> <td>Min/Max Version</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-input-group compact>
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select style="width: 125px" v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
</a-select> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-form-item> </a-select>
</td> <a-select style="width: 125px" v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
</tr> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
<tr> </a-select>
<td>MaxVersion</td> </a-input-group>
<td>
<a-form-item>
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
@@ -58,32 +53,30 @@
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint" <a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass"> style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option> <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-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
<tr> <tr style="line-height: 40px;">
<td>Multi Domain</td> <td>Multi Domain</td>
<td> <td>
<a-switch v-model="multiDomain"></a-switch> <a-switch v-model="multiDomain"></a-switch>
<a-button v-if="multiDomain" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button> <a-button v-if="multiDomain" style="margin-left: 10px" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
</td> </td>
</tr> </tr>
<tr v-if="multiDomain"> <tr v-if="multiDomain" style="line-height: 40px;">
<td colspan="2"> <td colspan="2" width="100%">
<a-form-item> <a-input-group style="margin-top:5px;" compact v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains"> <a-input style="width: 50%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'> </a-input>
<template slot="addonAfter"> <a-input style="width: 50%" v-model.trim="row.domain" placeholder='{{ i18n "host" }}'>
<a-button size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button> <a-button slot="addonAfter" size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</template>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item>
</td> </td>
</tr> </tr>
<tr v-else> <tr v-else>
@@ -101,7 +94,6 @@
<a-select <a-select
mode="multiple" mode="multiple"
style="width: 250px" style="width: 250px"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="inbound.stream.tls.alpn"> v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select> </a-select>
@@ -126,7 +118,7 @@
</tr> </tr>
<template v-for="cert,index in inbound.stream.tls.certs"> <template v-for="cert,index in inbound.stream.tls.certs">
<tr> <tr>
<td colspan="2"> <td colspan="2" width="100%">
<a-form-item label='{{ i18n "certificate" }}'> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid"> <a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button> <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
@@ -223,7 +215,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.reality.settings.fingerprint" <a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass"> style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

View File

@@ -2,34 +2,65 @@
<template slot="actions" slot-scope="text, client, index"> <template slot="actions" slot-scope="text, client, index">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template> <template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon> <a-icon style="font-size: 24px;" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template> <template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon> <a-icon style="font-size: 24px;" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "info" }}</template> <template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record.id,client);"></a-icon> <a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon> <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="color: blue"></a-icon>
<a-icon style="font-size: 24px;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
</a-popconfirm>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template> <template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon> <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" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
</a-popconfirm>
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="enable" slot-scope="text, client, index"> <template slot="enable" slot-scope="text, client, index">
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch> <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template> </template>
<template slot="online" slot-scope="text, client, index">
<template v-if="isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag>
</template>
<template v-else>
<a-tag>{{ i18n "offline" }}</a-tag>
</template>
</template>
<template slot="client" slot-scope="text, client"> <template slot="client" slot-scope="text, client">
<a-tooltip>
<template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="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 ]] [[ client.email ]]
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template> </template>
<template slot="traffic" slot-scope="text, client"> <template slot="traffic" slot-scope="text, client">
<a-popover :overlay-class-name="themeSwitcher.darkClass"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="client.email"> <template slot="content" v-if="client.email">
<table cellpadding="2" width="100%"> <table cellpadding="2" width="100%">
<tr> <tr>
@@ -38,24 +69,200 @@
</tr> </tr>
<tr v-if="client.totalGB > 0"> <tr v-if="client.totalGB > 0">
<td>{{ i18n "remained" }}</td> <td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td> <td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
</tr> </tr>
</table> </table>
</template> </template>
<a-tag :color="statsColor(record, client.email)"> <table>
[[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] / <tr>
<template v-if="client.totalGB > 0">[[client._totalGB]]GB</template> <td width="80px" style="margin:0; text-align: right;font-size: 1em;">
<template v-else>&infin;</template> [[ sizeFormat(getSumStats(record, client.email)) ]]
</a-tag> </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>
<td width="120px" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="statsColor(record, client.email)"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress
:show-info="false"
:status="isClientOnline(client.email)? 'active' : ''"
:percent="100"></a-progress>
</td>
<td width="60px">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
<span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
</td>
</tr>
</table>
</a-popover> </a-popover>
</template> </template>
<template slot="expiryTime" slot-scope="text, client, index"> <template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime > 0"> <template v-if="client.expiryTime !=0 && client.reset >0">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
[[ DateUtil.formatMillis(client._expiryTime) ]] <template slot="content">
</a-tag> <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) ]]
</td>
<td width="120px" class="infinite-bar">
<a-progress :show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
</tr>
</table>
</a-popover>
</template> </template>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag> <template v-else>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <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-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-popover>
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: 0;" class="infinite-tag">&infin;</a-tag>
</template>
</template>
<template slot="actionMenu" slot-scope="text, client, index">
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);">
<a-icon style="font-size: 14px;" type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<a-menu-item @click="openEditClient(record.id,client);">
<a-icon style="font-size: 14px;" type="edit"></a-icon>
{{ i18n "pages.client.edit" }}
</a-menu-item>
<a-menu-item @click="showInfo(record.id,client);">
<a-icon style="font-size: 14px;" type="info-circle"></a-icon>
{{ i18n "info" }}
</a-menu-item>
<a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0">
<a-icon style="font-size: 14px;" type="retweet"></a-icon>
{{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
<a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)">
<a-icon style="font-size: 14px;" type="delete"></a-icon>
<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>
{{ i18n "enable"}}
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="info" slot-scope="text, client, index">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content">
<table>
<tr>
<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="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>
<td width="120px" v-else-if="client.totalGB > 0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="client.email">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
<td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
</tr>
<tr>
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
</tr>
</table>
</template>
<a-progress :stroke-color="statsColor(record, client.email)"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : 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"
:status="isClientOnline(client.email)? 'active' : ''"
: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>
</td>
</tr>
<tr>
<td colspan="3" style="text-align: center;">
<a-divider style="margin: 0; border-collapse: separate;"></a-divider>
{{ i18n "pages.inbounds.expireDate" }}
</td>
</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="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-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-progress :show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</a-popover>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
</template>
<template v-else>
<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-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-popover>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">&infin;</a-tag>
</template>
</td>
</tr>
</table>
</template>
<a-badge>
<a-icon v-if="!client.enable" slot="count" type="pause-circle" :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-badge>
</a-popover>
</template> </template>
{{end}} {{end}}

View File

@@ -3,9 +3,9 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true" :closable="true"
:mask-closable="true" :mask-closable="true"
:class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="600px" width="600px"
:class="themeSwitcher.currentTheme"
> >
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px; width: 100%;">
<tr><td> <tr><td>
@@ -311,7 +311,7 @@
if(infoModal.clientStats){ if(infoModal.clientStats){
return infoModal.clientStats.enable; return infoModal.clientStats.enable;
} }
return infoModal.dbInbound.isEnable; return true;
}, },
get isEnable() { get isEnable() {
if(infoModal.clientSettings){ if(infoModal.clientSettings){

View File

@@ -1,8 +1,7 @@
{{define "inboundModal"}} {{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok" <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.darkCardClass" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
<script> <script>

View File

@@ -8,20 +8,49 @@
} }
} }
@media (max-width: 768px) {
.ant-card-body {
padding: .5rem;
}
}
.ant-col-sm-24 { .ant-col-sm-24 {
margin-top: 10px; margin: 0.5rem -2rem 0.5rem 2rem;
} }
tr.hideExpandIcon .ant-table-row-expand-icon { tr.hideExpandIcon .ant-table-row-expand-icon {
display: none; display: none;
} }
.infinite-tag {
padding: 0 5px;
border-radius: 2rem;
min-width: 50px;
}
.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: #3c1536;
border: #7a316f solid 1px;
}
.ant-collapse {
margin: 5px 0;
}
.online-animation .ant-badge-status-dot {
animation: 1.2s ease infinite normal none running onlineAnimation;
}
@keyframes onlineAnimation {
0%, 50%, 100% { transform: scale(1); opacity: 1; }
10% { transform: scale(1.5); opacity: .2; }
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px" <a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}' message='{{ i18n "secAlertTitle" }}'
@@ -32,53 +61,63 @@
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass"> <a-card hoverable>
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}: {{ i18n "pages.inbounds.totalDownUp" }}:
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag> <a-tag color="blue">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalUsage" }}: {{ i18n "pages.inbounds.totalUsage" }}:
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag> <a-tag color="blue">[[ sizeFormat(total.up + total.down) ]]</a-tag>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.inboundCount" }}: {{ i18n "pages.inbounds.inboundCount" }}:
<a-tag color="green">[[ dbInbounds.length ]]</a-tag> <a-tag color="blue">[[ dbInbounds.length ]]</a-tag>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}: {{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag> <a-tag color="blue">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag> <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag> <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template> </template>
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag> <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p>
</template>
<a-tag color="green" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
</a-popover>
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
<div slot="title"> <div slot="title">
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="12" :sm="12" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button> <a-button type="primary" icon="plus" @click="openAddInbound">
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
</a-button>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button> <a-button type="primary" icon="menu">
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme"> <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="export"> <a-menu-item key="export">
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
@@ -99,11 +138,11 @@
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;"> <a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval" <a-select v-model="refreshInterval"
v-if="isRefreshEnabled" v-if="isRefreshEnabled"
@change="changeRefreshInterval" style="width: 70px;"
:dropdown-class-name="themeSwitcher.darkCardClass"> @change="changeRefreshInterval" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option> <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select> </a-select>
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon> <a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
@@ -111,29 +150,35 @@
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
<a-switch v-model="enableFilter" <div style="display: flex; align-items: center; justify-content: flex-start;">
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}' <a-switch v-model="enableFilter"
@change="toggleFilter" style="margin-right: 10px;"> style="margin-right: .5rem;"
</a-switch> @change="toggleFilter">
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> <a-icon slot="checkedChildren" type="search"></a-icon>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"> <a-icon slot="unCheckedChildren" type="filter"></a-icon>
<a-radio-button value="">{{ i18n "none" }}</a-radio-button> </a-switch>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button> <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button> <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button> <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
</a-radio-group> <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
</a-radio-group>
</div>
<a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1200 }" :scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds) :pagination=pagination(searchedInbounds)
:expand-icon-as-cell="false" :expand-icon-as-cell="false"
:expand-row-by-click="false" :expand-row-by-click="false"
:expand-icon-column-index="0" :expand-icon-column-index="0"
:row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')" :indent-size="0"
style="margin-top: 20px"> :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
style="margin-top: 10px">
<template slot="action" slot-scope="text, dbInbound"> <template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon> <a-icon @click="e => e.preventDefault()" type="more" style="font-size: 20px; text-decoration: solid;"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme"> <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit"> <a-menu-item key="edit">
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
@@ -143,7 +188,7 @@
<a-icon type="qrcode"></a-icon> <a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }} {{ i18n "qrCode" }}
</a-menu-item> </a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser)"> <template v-if="dbInbound.isMultiUser()">
<a-menu-item key="addClient"> <a-menu-item key="addClient">
<a-icon type="user-add"></a-icon> <a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}} {{ i18n "pages.client.add"}}
@@ -182,42 +227,52 @@
<a-icon type="delete"></a-icon> {{ i18n "delete"}} <a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span> </span>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="isMobile">
<a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
{{ i18n "pages.inbounds.enable" }}
</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</template> </template>
<template slot="protocol" slot-scope="text, dbInbound"> <template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag> <a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag> <a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">tls</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">reality</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
</template> </template>
</template> </template>
<template slot="clients" slot-scope="text, dbInbound"> <template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]"> <template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</template> </template>
</template> </template>
<template slot="traffic" slot-scope="text, dbInbound"> <template slot="traffic" slot-scope="text, dbInbound">
<a-popover :overlay-class-name="themeSwitcher.darkClass"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<table cellpadding="2" width="100%"> <table cellpadding="2" width="100%">
<tr> <tr>
@@ -230,7 +285,7 @@
</tr> </tr>
</table> </table>
</template> </template>
<a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'"> <a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] / [[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0"> <template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]] [[ sizeFormat(dbInbound.total) ]]
@@ -243,35 +298,117 @@
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch> <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template> </template>
<template slot="expiryTime" slot-scope="text, dbInbound"> <template slot="expiryTime" slot-scope="text, dbInbound">
<template v-if="dbInbound.expiryTime > 0"> <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
<a-tag v-if="dbInbound.isExpiry" color="red"> <template slot="content">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]] [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
[[ remainedDays(dbInbound._expiryTime) ]]
</a-tag> </a-tag>
<a-tag v-else color="blue"> </a-popover>
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]] <a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
</a-tag> </template>
</template> <template slot="info" slot-scope="text, dbInbound">
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
<template slot="content">
<table cellpadding="2">
<tr>
<td>{{ i18n "pages.inbounds.protocol" }}</td>
<td>
<a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.port" }}</td>
<td><a-tag>[[ dbInbound.port ]]</a-tag></td>
</tr>
<tr v-if="clientCount[dbInbound.id]">
<td>{{ i18n "clients" }}</td>
<td>
<a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.traffic" }}</td>
<td>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<table cellpadding="2" width="100%">
<tr>
<td>↑[[ sizeFormat(dbInbound.up) ]]</td>
<td>↓[[ sizeFormat(dbInbound.down) ]]</td>
</tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td>
<td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
</tr>
</table>
</template>
<a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
<template v-if="dbInbound.total > 0">
[[ sizeFormat(dbInbound.total) ]]
</template>
<template v-else>&infin;</template>
</a-tag>
</a-popover>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.expireDate" }}</td>
<td>
<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>
</td>
</tr>
</table>
</template>
<a-badge>
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
<a-icon type="info"></a-icon>
</a-button>
</a-badge>
</a-popover>
</template> </template>
<template slot="expandedRowRender" slot-scope="record"> <template slot="expandedRowRender" slot-scope="record">
<a-table <a-table
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
:row-key="client => client.id" :row-key="client => client.id"
:columns="innerColumns" :columns="isMobile ? innerMobileColumns : innerColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
:pagination=pagination(getInboundClients(record)) :pagination=pagination(getInboundClients(record))
style="margin-left: 20px;" :style="isMobile ? 'margin: -16px -5px -17px;' : 'margin-left: 10px;'">
>
{{template "client_table"}}
</a-table>
<a-table
v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
:row-key="client => client.id"
:columns="innerTrojanColumns"
:data-source="getInboundClients(record)"
:pagination=pagination(getInboundClients(record))
style="margin-left: 20px;"
>
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
</template> </template>
@@ -290,7 +427,6 @@
align: 'right', align: 'right',
dataIndex: "id", dataIndex: "id",
width: 30, width: 30,
responsive: ["xs"],
}, { }, {
title: '{{ i18n "pages.inbounds.operate" }}', title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center', align: 'center',
@@ -319,7 +455,7 @@
}, { }, {
title: '{{ i18n "clients" }}', title: '{{ i18n "clients" }}',
align: 'left', align: 'left',
width: 40, width: 50,
scopedSlots: { customRender: 'clients' }, scopedSlots: { customRender: 'clients' },
}, { }, {
title: '{{ i18n "pages.inbounds.traffic" }}', title: '{{ i18n "pages.inbounds.traffic" }}',
@@ -329,26 +465,46 @@
}, { }, {
title: '{{ i18n "pages.inbounds.expireDate" }}', title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center', align: 'center',
width: 80, width: 40,
scopedSlots: { customRender: 'expiryTime' }, scopedSlots: { customRender: 'expiryTime' },
}]; }];
const mobileColums = [{
title: "ID",
align: 'right',
dataIndex: "id",
width: 10,
responsive: ["s"],
}, {
title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center',
width: 25,
scopedSlots: { customRender: 'action' },
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'left',
width: 70,
dataIndex: "remark",
}, {
title: '{{ i18n "pages.inbounds.info" }}',
align: 'center',
width: 10,
scopedSlots: { customRender: 'info' },
}];
const innerColumns = [ const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 50, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.enable" }}', width: 20, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "online" }}', width: 20, scopedSlots: { customRender: 'online' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 60, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 55, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UUID', width: 120, dataIndex: "id" },
]; ];
const innerTrojanColumns = [ const innerMobileColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 55, scopedSlots: { customRender: 'expiryTime' } },
{ title: '{{ i18n "password" }}', width: 165, dataIndex: "password" },
]; ];
const app = new Vue({ const app = new Vue({
@@ -369,6 +525,7 @@
defaultCert: '', defaultCert: '',
defaultKey: '', defaultKey: '',
clientCount: [], clientCount: [],
onlineClients: [],
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false, refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -382,6 +539,7 @@
tgBotEnable: false, tgBotEnable: false,
showAlert: false, showAlert: false,
pageSize: 0, pageSize: 0,
isMobile: window.innerWidth <= 768,
}, },
methods: { methods: {
loading(spinning = true) { loading(spinning = true) {
@@ -394,11 +552,19 @@
this.refreshing = false; this.refreshing = false;
return; return;
} }
await this.getOnlineUsers();
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
setTimeout(() => { setTimeout(() => {
this.refreshing = false; this.refreshing = false;
}, 500); }, 500);
}, },
async getOnlineUsers() {
const msg = await HttpUtil.post('/xui/inbound/onlines');
if (!msg.success) {
return;
}
this.onlineClients = msg.obj != null ? msg.obj : [];
},
async getDefaultSettings() { async getDefaultSettings() {
const msg = await HttpUtil.post('/xui/setting/defaultSettings'); const msg = await HttpUtil.post('/xui/setting/defaultSettings');
if (!msg.success) { if (!msg.success) {
@@ -443,7 +609,7 @@
} }
}, },
getClientCounts(dbInbound, inbound) { getClientCounts(dbInbound, inbound) {
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = []; let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [];
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
clientStats = dbInbound.clientStats clientStats = dbInbound.clientStats
now = new Date().getTime() now = new Date().getTime()
@@ -452,6 +618,7 @@
if (dbInbound.enable) { if (dbInbound.enable) {
clients.forEach(client => { clients.forEach(client => {
client.enable ? active.push(client.email) : deactive.push(client.email); client.enable ? active.push(client.email) : deactive.push(client.email);
if(this.isClientOnline(client.email)) online.push(client.email);
}); });
clientStats.forEach(client => { clientStats.forEach(client => {
if (!client.enable) { if (!client.enable) {
@@ -473,6 +640,7 @@
deactive: deactive, deactive: deactive,
depleted: depleted, depleted: depleted,
expiring: expiring, expiring: expiring,
online: online,
}; };
}, },
searchInbounds(key) { searchInbounds(key) {
@@ -549,10 +717,10 @@
clickAction(action, dbInbound) { clickAction(action, dbInbound) {
switch (action.key) { switch (action.key) {
case "qrcode": case "qrcode":
this.showQrcode(dbInbound); this.showQrcode(dbInbound.id);
break; break;
case "showInfo": case "showInfo":
this.showInfo(dbInbound); this.showInfo(dbInbound.id);
break; break;
case "edit": case "edit":
this.openEditInbound(dbInbound.id); this.openEditInbound(dbInbound.id);
@@ -618,6 +786,7 @@
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"', title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}', content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.update"}}', okText: '{{ i18n "pages.inbounds.update"}}',
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel" }}', cancelText: '{{ i18n "cancel" }}',
onOk: () => { onOk: () => {
const baseInbound = dbInbound.toInbound(); const baseInbound = dbInbound.toInbound();
@@ -754,7 +923,7 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.darkCardClass, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => { onOk: () => {
@@ -769,23 +938,27 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: themeSwitcher.darkCardClass, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId), onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId, client) { delClient(dbInboundId, client,confirmation = true) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = this.getClientId(dbInbound.protocol, client); clientId = this.getClientId(dbInbound.protocol, client);
this.$confirm({ if (confirmation){
title: '{{ i18n "pages.inbounds.deleteInbound"}}', this.$confirm({
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', title: '{{ i18n "pages.inbounds.deleteClient"}}',
class: themeSwitcher.darkCardClass, content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
okText: '{{ i18n "delete"}}', class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}', okText: '{{ i18n "delete"}}',
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`), cancelText: '{{ i18n "cancel"}}',
}); onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
});
} else {
this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`);
}
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch (protocol) { switch (protocol) {
@@ -832,9 +1005,12 @@
}, },
showInfo(dbInboundId, client) { showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound(); index=0;
clients = this.getClients(dbInbound.protocol, inbound.settings); if (dbInbound.isMultiUser()){
index = this.findIndexOfClient(dbInbound.protocol, clients, client); inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
}
newDbInbound = this.checkFallback(dbInbound); newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index); infoModal.show(newDbInbound, index);
}, },
@@ -870,21 +1046,25 @@
return dbInbound.toInbound().settings.shadowsockses; return dbInbound.toInbound().settings.shadowsockses;
} }
}, },
resetClientTraffic(client, dbInboundId) { resetClientTraffic(client, dbInboundId, confirmation = true) {
this.$confirm({ if (confirmation){
title: '{{ i18n "pages.inbounds.resetTraffic"}}', this.$confirm({
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
class: themeSwitcher.darkCardClass, content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
okText: '{{ i18n "reset"}}', class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}', okText: '{{ i18n "reset"}}',
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email), cancelText: '{{ i18n "cancel"}}',
}) onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
})
} else {
this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
}, },
resetAllTraffic() { resetAllTraffic() {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}', title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
class: themeSwitcher.darkCardClass, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllTraffics'), onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
@@ -894,7 +1074,7 @@
this.$confirm({ this.$confirm({
title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}', title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}', content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: themeSwitcher.darkCardClass, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId), onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
@@ -904,36 +1084,98 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}', title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}', content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: themeSwitcher.darkCardClass, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId), onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
}) })
}, },
isExpiry(dbInbound, index) { isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index) return dbInbound.toInbound().isExpiry(index);
}, },
getUpStats(dbInbound, email) { getUpStats(dbInbound, email) {
if (email.length == 0) return 0 if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.up : 0 return clientStats ? clientStats.up : 0;
}, },
getDownStats(dbInbound, email) { getDownStats(dbInbound, email) {
if (email.length == 0) return 0 if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.down : 0 return clientStats ? clientStats.down : 0;
},
getSumStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.up + clientStats.down : 0;
},
getRemStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return 0;
remained = clientStats.totalGB - (clientStats.up + clientStats.down);
return remained>0 ? remained : 0;
}, },
statsColor(dbInbound, email) { statsColor(dbInbound, email) {
if (email.length == 0) return 'blue'; if (email.length == 0) return '#0e49b5';
clientStats = dbInbound.clientStats.find(stats => stats.email === email); clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); switch (true) {
case !clientStats:
return "#0e49b5";
case clientStats.up + clientStats.down < clientStats.total - app.trafficDiff:
return "#0e49b5";
case clientStats.up + clientStats.down < clientStats.total:
return "#FFA031";
default:
return "#E04141";
}
},
statsProgress(dbInbound, email) {
if (email.length == 0) return 100;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return 0;
if (clientStats.total == 0) return 100;
return 100*(clientStats.down + clientStats.up)/clientStats.total;
},
expireProgress(expTime, reset) {
now = new Date().getTime();
remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000;
resetSeconds = reset * 86400;
if (remainedSeconds >= resetSeconds) return 0;
return 100*(1-(remainedSeconds/resetSeconds));
},
remainedDays(expTime){
if (expTime == 0) return null;
if (expTime < 0) return formatSecond(expTime/-1000);
now = new Date().getTime();
if (expTime < now) return '{{ i18n "depleted" }}';
return formatSecond((expTime-now)/1000);
},
statsExpColor(dbInbound, email){
if (email.length == 0) return '#7a316f';
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return '#7a316f';
statsColor = usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
expColor = usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
switch (true) {
case statsColor == "red" || expColor == "red":
return "#E04141";
case statsColor == "orange" || expColor == "orange":
return "#FFA031";
case statsColor == "blue" || expColor == "blue":
return "#0e49b5";
default:
return "#7a316f";
}
}, },
isClientEnabled(dbInbound, email) { isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
return clientStats ? clientStats['enable'] : true return clientStats ? clientStats['enable'] : true;
},
isClientOnline(email) {
return this.onlineClients.includes(email);
}, },
isRemovable(dbInboundId) { isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1 return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
}, },
inboundLinks(dbInboundId) { inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -943,7 +1185,7 @@
exportAllLinks() { exportAllLinks() {
let copyText = ''; let copyText = '';
for (const dbInbound of this.dbInbounds) { for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks copyText += dbInbound.genInboundLinks;
} }
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds'); txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
}, },
@@ -976,7 +1218,7 @@
pagination(obj){ pagination(obj){
if (this.pageSize > 0 && obj.length>this.pageSize) { if (this.pageSize > 0 && obj.length>this.pageSize) {
// Set page options based on object size // Set page options based on object size
sizeOptions = [] sizeOptions = [];
for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) { for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
sizeOptions.push(i.toString()); sizeOptions.push(i.toString());
} }
@@ -989,10 +1231,13 @@
position: 'bottom', position: 'bottom',
pageSize: this.pageSize, pageSize: this.pageSize,
pageSizeOptions: sizeOptions pageSizeOptions: sizeOptions
} };
return p return p;
} }
return false return false
},
onResize() {
this.isMobile = window.innerWidth <= 768;
} }
}, },
watch: { watch: {
@@ -1004,6 +1249,8 @@
if (window.location.protocol !== "https:") { if (window.location.protocol !== "https:") {
this.showAlert = true; this.showAlert = true;
} }
window.addEventListener('resize', this.onResize);
this.onResize();
this.loading(); this.loading();
this.getDefaultSettings(); this.getDefaultSettings();
if (this.isRefreshEnabled) { if (this.isRefreshEnabled) {

View File

@@ -6,6 +6,9 @@
.ant-layout-content { .ant-layout-content {
margin: 24px 16px; margin: 24px 16px;
} }
.ant-card-hoverable {
margin-inline: 0.3rem;
}
} }
.ant-col-sm-24 { .ant-col-sm-24 {
@@ -17,9 +20,9 @@
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear> <transition name="list" appear>
@@ -33,21 +36,19 @@
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU: ([[ status.cpuCount ]]core)</div> <div>CPU: ([[ status.cpuCount ]]core)</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:class="themeSwitcher.darkCardClass"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -60,7 +61,6 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:class="themeSwitcher.darkCardClass"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
<div> <div>
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -69,7 +69,6 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:class="themeSwitcher.darkCardClass"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -84,16 +83,16 @@
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
X-UI: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="green">{{ .cur_ver }}</a-tag></a> X-UI: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="blue">{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag> Xray: <a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
{{ i18n "pages.index.operationHours" }}: {{ i18n "pages.index.operationHours" }}:
Xray Xray
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag> <a-tag color="blue">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
OS OS
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -101,11 +100,11 @@
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag> <a-tag color="blue">[[ formatSecond(status.uptime) ]]</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
{{ i18n "pages.index.xrayStatus" }}: {{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag> <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error"> <a-tooltip v-if="status.xray.state === State.Error">
@@ -114,26 +113,26 @@
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
{{ i18n "menu.link" }}: {{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
{{ i18n "usage"}}: {{ i18n "usage"}}:
Memory: [[ sizeFormat(status.appStats.mem) ]] - Memory: [[ sizeFormat(status.appStats.mem) ]] -
Threads: [[ status.appStats.threads ]] Threads: [[ status.appStats.threads ]]
@@ -141,7 +140,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
Host: [[ status.hostInfo.hostname ]] - Host: [[ status.hostInfo.hostname ]] -
<template v-if="status.hostInfo.ipv4">IPv4: <template v-if="status.hostInfo.ipv4">IPv4:
<a-tooltip> <a-tooltip>
@@ -162,7 +161,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
{{ i18n "pages.index.connectionCount" }}: TCP: [[ status.tcpCount ]] UDP: [[ status.udpCount ]] {{ i18n "pages.index.connectionCount" }}: TCP: [[ status.tcpCount ]] UDP: [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -173,7 +172,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="arrow-up"></a-icon> <a-icon type="arrow-up"></a-icon>
@@ -199,7 +198,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass"> <a-card hoverable>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="cloud-upload"></a-icon> <a-icon type="cloud-upload"></a-icon>
@@ -231,12 +230,12 @@
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false" :closable="true" @ok="() => versionModal.visible = false"
:class="themeSwitcher.darkCardClass" :class="themeSwitcher.currentTheme"
footer=""> footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions"> <template v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'blue' : 'green'" <a-tag :color="index % 2 == 0 ? 'purple' : 'blue'"
style="margin: 10px" @click="switchV2rayVersion(version)"> style="margin: 10px" @click="switchV2rayVersion(version)">
[[ version ]] [[ version ]]
</a-tag> </a-tag>
@@ -245,15 +244,14 @@
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs" <a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false" :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="themeSwitcher.darkCardClass" :class="themeSwitcher.currentTheme"
width="800px" width="800px"
footer=""> footer="">
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Count"> <a-form-item label="Count">
<a-select v-model="logModal.rows" <a-select v-model="logModal.rows"
style="width: 80px" style="width: 80px"
@change="openLogs()" @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="10">10</a-select-option> <a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option> <a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option> <a-select-option value="50">50</a-select-option>
@@ -263,8 +261,7 @@
<a-form-item label="Log Level"> <a-form-item label="Log Level">
<a-select v-model="logModal.level" <a-select v-model="logModal.level"
style="width: 120px" style="width: 120px"
@change="openLogs()" @change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="debug">Debug</a-select-option> <a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option> <a-select-option value="info">Info</a-select-option>
<a-select-option value="warning">Warning</a-select-option> <a-select-option value="warning">Warning</a-select-option>
@@ -289,12 +286,13 @@
</a-modal> </a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title" <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" :class="themeSwitcher.darkCardClass" :closable="true"
:class="themeSwitcher.currentTheme"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()"> @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;"> <a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon> :message="backupModal.description"
[[ backupModal.description ]] show-icon
</p> ></a-alert>
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;"> <a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()"> <a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]] [[ backupModal.exportText ]]
@@ -335,11 +333,11 @@
get color() { get color() {
const percent = this.percent; const percent = this.percent;
if (percent < 80) { if (percent < 80) {
return '#67C23A'; return '#0e49b5';
} else if (percent < 90) { } else if (percent < 90) {
return '#E6A23C'; return '#ffa031';
} else { } else {
return '#F56C6C'; return '#e04141';
} }
} }
} }
@@ -382,7 +380,7 @@
this.xray = data.xray; this.xray = data.xray;
switch (this.xray.state) { switch (this.xray.state) {
case State.Running: case State.Running:
this.xray.color = "green"; this.xray.color = "blue";
break; break;
case State.Stop: case State.Stop:
this.xray.color = "orange"; this.xray.color = "orange";
@@ -487,8 +485,8 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
class: themeSwitcher.currentTheme,
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: themeSwitcher.darkCardClass,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();

View File

@@ -8,8 +8,11 @@
} }
} }
.ant-col-sm-24 { @media (max-width: 768px) {
margin-top: 10px; .ant-tabs-nav .ant-tabs-tab {
margin: 0;
padding: 12px .5rem;
}
} }
.ant-tabs-bar { .ant-tabs-bar {
@@ -20,19 +23,6 @@
display: block; display: block;
} }
.alert-msg {
color: inherit;
font-weight: bold;
font-size: 18px;
padding: 20px 20px;
text-align: center;
}
.alert-msg > i {
color: inherit;
font-size: 24px;
}
.collapse-title { .collapse-title {
color: inherit; color: inherit;
font-weight: bold; font-weight: bold;
@@ -47,11 +37,11 @@
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="themeSwitcher.bgStyle"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px" <a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}' message='{{ i18n "secAlertTitle" }}'
@@ -62,19 +52,25 @@
</a-alert> </a-alert>
</transition> </transition>
<a-space direction="vertical"> <a-space direction="vertical">
<a-space direction="horizontal"> <a-card hoverable style="margin-bottom: .5rem;">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> <a-row>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> <a-col :xs="24" :sm="8" style="padding: 4px;">
</a-space> <a-space direction="horizontal">
<a-tabs default-active-key="1" :class="themeSwitcher.darkCardClass"> <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
</a-col>
<a-col :xs="24" :sm="16">
<a-alert type="warning" style="float: right; width: fit-content"
message='{{ i18n "pages.settings.infoDesc" }}'
show-icon
>
</a-col>
</a-row>
</a-card>
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'> <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-list item-layout="horizontal">
<h2 class="alert-msg">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
@@ -98,9 +94,7 @@
ref="selectLang" ref="selectLang"
v-model="lang" v-model="lang"
@change="setLang(lang)" @change="setLang(lang)"
:dropdown-class-name="themeSwitcher.darkCardClass" style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
@@ -113,7 +107,7 @@
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.settings.userSettings"}}'> <a-tab-pane key="2" tab='{{ i18n "pages.settings.userSettings"}}'>
<a-form :style="'padding: 20px;' + themeSwitcher.textStyle"> <a-form style="padding: 20px;">
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'> <a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input> <a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item> </a-form-item>
@@ -131,172 +125,8 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'> <a-list item-layout="horizontal">
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="alert-msg">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
</h2>
</a-row>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}'
description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="freedomStrategy"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%">
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}'
description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="routingStrategy"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%">
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.blockConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
</a-space>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.manualListsDesc" }}
</h2>
</a-row>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
<setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
<setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-tab-pane>
</a-tabs>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 class="alert-msg">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
@@ -315,8 +145,8 @@
<a-select <a-select
ref="selectBotLang" ref="selectBotLang"
v-model="allSetting.tgLang" v-model="allSetting.tgLang"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%" style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme"
> >
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
@@ -329,14 +159,8 @@
</a-list-item> </a-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'> <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-list item-layout="horizontal">
<h2 class="alert-msg">
<a-icon type="warning"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
@@ -373,62 +197,7 @@
saveBtnDisable: true, saveBtnDisable: true,
user: {}, user: {},
lang: getLang(), lang: getLang(),
showAlert: false, showAlert: false
ipv4Settings: {
tag: "IPv4",
protocol: "freedom",
settings: {
domainStrategy: "UseIPv4"
}
},
directSettings: {
tag: "direct",
protocol: "freedom"
},
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
},
ips: {
local: ["geoip:private"],
cn: ["geoip:cn"],
ir: ["geoip:ir"],
ru: ["geoip:ru"],
},
domains: {
ads: [
"geosite:category-ads-all",
"ext:iran.dat:ads"
],
google: ["geosite:google"],
netflix: ["geosite:netflix"],
cn: [
"geosite:cn",
"regexp:.*\\.cn$"
],
ru: [
"geosite:category-gov-ru",
"regexp:.*\\.ru$"
],
ir: [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"geosite:category-ir"
]
},
familyProtectDNS: {
"servers": [
"1.1.1.3",
"1.0.0.3",
"94.140.14.15",
"94.140.15.16"
],
"queryStrategy": "UseIPv4"
},
}
}, },
methods: { methods: {
loading(spinning = true) { loading(spinning = true) {
@@ -466,6 +235,7 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.settings.restartPanel" }}', title: '{{ i18n "pages.settings.restartPanel" }}',
content: '{{ i18n "pages.settings.restartPanelDesc" }}', content: '{{ i18n "pages.settings.restartPanelDesc" }}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "sure" }}', okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}', cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(), onOk: () => resolve(),
@@ -477,88 +247,13 @@
if (msg.success) { if (msg.success) {
this.loading(true); this.loading(true);
await PromiseUtil.sleep(5000); await PromiseUtil.sleep(5000);
const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
if (host == this.oldAllSetting.webDomain) host = null;
if (port == this.oldAllSetting.webPort) port = null;
const isTLS = webCertFile !== "" || webKeyFile !== ""; const isTLS = webCertFile !== "" || webKeyFile !== "";
const url = buildURL({ host, port, isTLS, base, path: "xui/settings" }); const url = buildURL({ host, port, isTLS, base, path: "xui/settings" });
window.location.replace(url); window.location.replace(url);
} }
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
syncRulesWithOutbound(tag, setting) {
const newTemplateSettings = this.templateSettings;
const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
if (!haveRules && outboundIndex > 0) {
newTemplateSettings.outbounds.splice(outboundIndex);
}
if (haveRules && outboundIndex < 0) {
newTemplateSettings.outbounds.push(setting);
}
this.templateSettings = newTemplateSettings;
},
templateRuleGetter(routeSettings) {
const { property, outboundTag } = routeSettings;
let result = [];
if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
result.push(...routingRule[property]);
}
}
);
}
return result;
},
templateRuleSetter(routeSettings) {
const { data, property, outboundTag } = routeSettings;
const oldTemplateSettings = this.templateSettings;
const newTemplateSettings = oldTemplateSettings;
currentProperty = this.templateRuleGetter({ outboundTag, property })
if (currentProperty.length == 0) {
const propertyRule = {
type: "field",
outboundTag,
[property]: data
};
newTemplateSettings.routing.rules.push(propertyRule);
}
else {
const newRules = [];
insertedOnce = false;
newTemplateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (!insertedOnce && data.length > 0) {
insertedOnce = true;
routingRule[property] = data;
newRules.push(routingRule);
}
}
else {
newRules.push(routingRule);
}
}
);
newTemplateSettings.routing.rules = newRules;
}
this.templateSettings = newTemplateSettings;
} }
}, },
async mounted() { async mounted() {
@@ -570,357 +265,7 @@
await PromiseUtil.sleep(1000); await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
} }
}, }
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue);
this.templateSettings = newTemplateSettings;
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue);
this.templateSettings = newTemplateSettings;
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue);
this.templateSettings = newTemplateSettings;
},
},
freedomStrategy: {
get: function () {
if (!this.templateSettings) return "AsIs";
freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
if (!freedomOutbound) return "AsIs";
if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
return freedomOutbound.settings.domainStrategy;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag);
if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
} else {
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
}
this.templateSettings = newTemplateSettings;
}
},
routingStrategy: {
get: function () {
if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
return this.templateSettings.routing.domainStrategy;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.domainStrategy = newValue;
this.templateSettings = newTemplateSettings;
}
},
blockedIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
}
},
blockedDomains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
}
},
blockedProtocols: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
}
},
directIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
this.syncRulesWithOutbound("direct", this.directSettings);
}
},
directDomains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
this.syncRulesWithOutbound("direct", this.directSettings);
}
},
ipv4Domains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
}
},
manualBlockedIPs: {
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
},
manualBlockedDomains: {
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
},
manualDirectIPs: {
get: function () { return JSON.stringify(this.directIPs, null, 2); },
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
},
manualDirectDomains: {
get: function () { return JSON.stringify(this.directDomains, null, 2); },
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
},
manualIPv4Domains: {
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
},
torrentSettings: {
get: function () {
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
},
set: function (newValue) {
if (newValue) {
this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
} else {
this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
}
},
},
privateIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
}
},
},
AdsSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
}
},
},
familyProtectSettings: {
get: function () {
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
if (newValue) {
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
} else {
delete newTemplateSettings.dns;
}
this.templateSettings = newTemplateSettings;
},
},
GoogleIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
},
set: function (newValue) {
if (newValue) {
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
} else {
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
}
},
},
NetflixIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
},
set: function (newValue) {
if (newValue) {
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
} else {
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
}
},
},
IRIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
IRIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
},
}); });
</script> </script>
</body> </body>

783
web/html/xui/xray.html Normal file
View File

@@ -0,0 +1,783 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
@media (max-width: 768px) {
.ant-tabs-nav .ant-tabs-tab {
margin: 0;
padding: 12px .5rem;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.collapse-title {
color: inherit;
font-weight: bold;
font-size: 18px;
padding: 10px 20px;
border-bottom: 2px solid;
}
.collapse-title > i {
color: inherit;
font-size: 24px;
}
</style>
<body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
{{ template "commonSider" . }}
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition>
<a-space direction="vertical">
<a-card hoverable style="margin-bottom: .5rem;">
<a-row>
<a-col :xs="24" :sm="8" style="padding: 4px;">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
</a-col>
<a-col :xs="24" :sm="16">
<a-alert type="warning" style="float: right; width: fit-content"
message='{{ i18n "pages.settings.infoDesc" }}'
show-icon
>
</a-col>
</a-row>
</a-card>
<a-tabs default-active-key="1">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.generalConfigsDesc" }}
</template>
</a-alert>
</a-row>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.FreedomStrategy" }}'
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="freedomStrategy"
style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.RoutingStrategy" }}'
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="routingStrategy"
style="width: 100%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.generalConfigsDesc" }}
</template>
</a-alert>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Torrent"}}' desc='{{ i18n "pages.xray.TorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.PrivateIp"}}' desc='{{ i18n "pages.xray.PrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Ads"}}' desc='{{ i18n "pages.xray.AdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.Family"}}' desc='{{ i18n "pages.xray.FamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.blockCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.blockCountryConfigsDesc" }}
</template>
</a-alert>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.IRIp"}}' desc='{{ i18n "pages.xray.IRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.IRDomain"}}' desc='{{ i18n "pages.xray.IRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.ChinaIp"}}' desc='{{ i18n "pages.xray.ChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.ChinaDomain"}}' desc='{{ i18n "pages.xray.ChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.RussiaIp"}}' desc='{{ i18n "pages.xray.RussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.RussiaDomain"}}' desc='{{ i18n "pages.xray.RussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.directCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.directCountryConfigsDesc" }}
</template>
</a-alert>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectIRIp"}}' desc='{{ i18n "pages.xray.DirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectIRDomain"}}' desc='{{ i18n "pages.xray.DirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectChinaIp"}}' desc='{{ i18n "pages.xray.DirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectChinaDomain"}}' desc='{{ i18n "pages.xray.DirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectRussiaIp"}}' desc='{{ i18n "pages.xray.DirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.DirectRussiaDomain"}}' desc='{{ i18n "pages.xray.DirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.ipv4ConfigsDesc" }}
</template>
</a-alert>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.GoogleIPv4"}}' desc='{{ i18n "pages.xray.GoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.NetflixIPv4"}}' desc='{{ i18n "pages.xray.NetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
</a-space>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.xray.manualLists"}}' style="padding-top: 20px;">
<a-row :xs="24" :sm="24" :lg="12" style="margin-bottom: 10px;">
<a-alert type="warning" style="float: left; width: fit-content">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.manualListsDesc" }}
</template>
</a-alert>
</a-row>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.manualBlockedIPs"}}'>
<setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.manualBlockedDomains"}}'>
<setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.manualDirectIPs"}}'>
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.manualDirectDomains"}}'>
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.manualIPv4Domains"}}'>
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.Inbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Inbounds"}}' desc='{{ i18n "pages.xray.InboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.Outbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Outbounds"}}' desc='{{ i18n "pages.xray.OutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.Routings"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Routings"}}' desc='{{ i18n "pages.xray.RoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.completeTemplate"}}' style="padding-top: 20px;">
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Template"}}' desc='{{ i18n "pages.xray.TemplateDesc"}}' v-model="xraySetting"></setting-list-item>
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/setting"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
themeSwitcher,
spinning: false,
oldXraySetting: '',
xraySetting: '',
saveBtnDisable: true,
showAlert: false,
ipv4Settings: {
tag: "IPv4",
protocol: "freedom",
settings: {
domainStrategy: "UseIPv4"
}
},
directSettings: {
tag: "direct",
protocol: "freedom"
},
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
},
ips: {
local: ["geoip:private"],
cn: ["geoip:cn"],
ir: ["geoip:ir"],
ru: ["geoip:ru"],
},
domains: {
ads: [
"geosite:category-ads-all",
"ext:iran.dat:ads"
],
google: ["geosite:google"],
netflix: ["geosite:netflix"],
cn: [
"geosite:cn",
"regexp:.*\\.cn$"
],
ru: [
"geosite:category-gov-ru",
"regexp:.*\\.ru$"
],
ir: [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"geosite:category-ir"
]
},
familyProtectDNS: {
"servers": [
"1.1.1.3",
"1.0.0.3",
"94.140.14.15",
"94.140.15.16"
],
"queryStrategy": "UseIPv4"
},
}
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
},
async getXraySetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/xray/");
this.loading(false);
if (msg.success) {
this.oldXraySetting = msg.obj;
this.xraySetting = msg.obj;
this.saveBtnDisable = true;
}
},
async updateXraySetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/xray/update", {xraySetting : this.xraySetting});
this.loading(false);
if (msg.success) {
await this.getXraySetting();
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.settings.restartPanel" }}',
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/xray/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
syncRulesWithOutbound(tag, setting) {
const newTemplateSettings = this.templateSettings;
const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
if (!haveRules && outboundIndex > 0) {
newTemplateSettings.outbounds.splice(outboundIndex);
}
if (haveRules && outboundIndex < 0) {
newTemplateSettings.outbounds.push(setting);
}
this.templateSettings = newTemplateSettings;
},
templateRuleGetter(routeSettings) {
const { property, outboundTag } = routeSettings;
let result = [];
if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
result.push(...routingRule[property]);
}
}
);
}
return result;
},
templateRuleSetter(routeSettings) {
const { data, property, outboundTag } = routeSettings;
const oldTemplateSettings = this.templateSettings;
const newTemplateSettings = oldTemplateSettings;
currentProperty = this.templateRuleGetter({ outboundTag, property })
if (currentProperty.length == 0) {
const propertyRule = {
type: "field",
outboundTag,
[property]: data
};
newTemplateSettings.routing.rules.push(propertyRule);
}
else {
const newRules = [];
insertedOnce = false;
newTemplateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (!insertedOnce && data.length > 0) {
insertedOnce = true;
routingRule[property] = data;
newRules.push(routingRule);
}
}
else {
newRules.push(routingRule);
}
}
);
newTemplateSettings.routing.rules = newRules;
}
this.templateSettings = newTemplateSettings;
}
},
async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
await this.getXraySetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
}
},
computed: {
templateSettings: {
get: function () { return this.xraySetting ? JSON.parse(this.xraySetting) : null; },
set: function (newValue) { this.xraySetting = JSON.stringify(newValue, null, 2); },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue);
this.templateSettings = newTemplateSettings;
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue);
this.templateSettings = newTemplateSettings;
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue);
this.templateSettings = newTemplateSettings;
},
},
freedomStrategy: {
get: function () {
if (!this.templateSettings) return "AsIs";
freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
if (!freedomOutbound) return "AsIs";
if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
return freedomOutbound.settings.domainStrategy;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag);
if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
} else {
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
}
this.templateSettings = newTemplateSettings;
}
},
routingStrategy: {
get: function () {
if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
return this.templateSettings.routing.domainStrategy;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.domainStrategy = newValue;
this.templateSettings = newTemplateSettings;
}
},
blockedIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
}
},
blockedDomains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
}
},
blockedProtocols: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
}
},
directIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
this.syncRulesWithOutbound("direct", this.directSettings);
}
},
directDomains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
this.syncRulesWithOutbound("direct", this.directSettings);
}
},
ipv4Domains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
}
},
manualBlockedIPs: {
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
},
manualBlockedDomains: {
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
},
manualDirectIPs: {
get: function () { return JSON.stringify(this.directIPs, null, 2); },
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
},
manualDirectDomains: {
get: function () { return JSON.stringify(this.directDomains, null, 2); },
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
},
manualIPv4Domains: {
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
},
torrentSettings: {
get: function () {
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
},
set: function (newValue) {
if (newValue) {
this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
} else {
this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
}
},
},
privateIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
}
},
},
AdsSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
}
},
},
familyProtectSettings: {
get: function () {
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
if (newValue) {
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
} else {
delete newTemplateSettings.dns;
}
this.templateSettings = newTemplateSettings;
},
},
GoogleIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
},
set: function (newValue) {
if (newValue) {
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
} else {
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
}
},
},
NetflixIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
},
set: function (newValue) {
if (newValue) {
this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
} else {
this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
}
},
},
IRIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
IRIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
},
});
</script>
</body>
</html>

View File

@@ -41,7 +41,7 @@
} }
}, },
"routing": { "routing": {
"domainStrategy": "IPIfNonMatch", "domainStrategy": "AsIs",
"rules": [ "rules": [
{ {
"type": "field", "type": "field",

View File

@@ -249,7 +249,18 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
tag := oldInbound.Tag tag := oldInbound.Tag
err = s.updateClientTraffics(oldInbound, inbound) db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
err = s.updateClientTraffics(tx, oldInbound, inbound)
if err != nil { if err != nil {
return inbound, false, err return inbound, false, err
} }
@@ -290,11 +301,10 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
} }
s.xrayApi.Close() s.xrayApi.Close()
db := database.GetDB() return inbound, needRestart, tx.Save(oldInbound).Error
return inbound, needRestart, db.Save(oldInbound).Error
} }
func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbound *model.Inbound) error { func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error {
oldClients, err := s.GetClients(oldInbound) oldClients, err := s.GetClients(oldInbound)
if err != nil { if err != nil {
return err return err
@@ -304,17 +314,6 @@ func (s *InboundService) updateClientTraffics(oldInbound *model.Inbound, newInbo
return err return err
} }
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
var emailExists bool var emailExists bool
for _, oldClient := range oldClients { for _, oldClient := range oldClients {
@@ -581,7 +580,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
if len(clients[0].Email) > 0 { if len(clients[0].Email) > 0 {
if len(oldEmail) > 0 { if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0]) err = s.UpdateClientStat(tx, oldEmail, &clients[0])
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -648,6 +647,13 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
return err, false return err, false
} }
needRestart0, count, err := s.autoRenewClients(tx)
if err != nil {
logger.Warning("Error in renew clients:", err)
} else if count > 0 {
logger.Debugf("%v clients renewed", count)
}
needRestart1, count, err := s.disableInvalidClients(tx) needRestart1, count, err := s.disableInvalidClients(tx)
if err != nil { if err != nil {
logger.Warning("Error in disabling invalid clients:", err) logger.Warning("Error in disabling invalid clients:", err)
@@ -661,7 +667,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
} else if count > 0 { } else if count > 0 {
logger.Debugf("%v inbounds disabled", count) logger.Debugf("%v inbounds disabled", count)
} }
return nil, (needRestart1 || needRestart2) return nil, (needRestart0 || needRestart1 || needRestart2)
} }
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error { func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@@ -688,9 +694,15 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) { func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 { if len(traffics) == 0 {
// Empty onlineUsers
if p != nil {
p.SetOnlineClients(nil)
}
return nil return nil
} }
var onlineClients []string
emails := make([]string, 0, len(traffics)) emails := make([]string, 0, len(traffics))
for _, traffic := range traffics { for _, traffic := range traffics {
emails = append(emails, traffic.Email) emails = append(emails, traffic.Email)
@@ -716,11 +728,19 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email { if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
// Add user in onlineUsers array on traffic
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
onlineClients = append(onlineClients, traffics[traffic_index].Email)
}
break break
} }
} }
} }
// Set onlineUsers
p.SetOnlineClients(onlineClients)
err = tx.Save(dbClientTraffics).Error err = tx.Save(dbClientTraffics).Error
if err != nil { if err != nil {
logger.Warning("AddClientTraffic update data ", err) logger.Warning("AddClientTraffic update data ", err)
@@ -781,6 +801,102 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
return dbClientTraffics, nil return dbClientTraffics, nil
} }
func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) {
// check for time expired
var traffics []*xray.ClientTraffic
now := time.Now().Unix() * 1000
var err, err1 error
err = tx.Model(xray.ClientTraffic{}).Where("reset > 0 and expiry_time > 0 and expiry_time <= ?", now).Find(&traffics).Error
if err != nil {
return false, 0, err
}
// return if there is no client to renew
if len(traffics) == 0 {
return false, 0, nil
}
var inbound_ids []int
var inbounds []*model.Inbound
needRestart := false
var clientsToAdd []struct {
protocol string
tag string
client map[string]interface{}
}
for _, traffic := range traffics {
inbound_ids = append(inbound_ids, traffic.InboundId)
}
err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error
if err != nil {
return false, 0, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients := settings["clients"].([]interface{})
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index, traffic := range traffics {
if traffic.Email == c["email"].(string) {
newExpiryTime := traffic.ExpiryTime
for newExpiryTime < now {
newExpiryTime += (int64(traffic.Reset) * 86400000)
}
c["expiryTime"] = newExpiryTime
traffics[traffic_index].ExpiryTime = newExpiryTime
traffics[traffic_index].Down = 0
traffics[traffic_index].Up = 0
if !traffic.Enable {
traffics[traffic_index].Enable = true
clientsToAdd = append(clientsToAdd,
struct {
protocol string
tag string
client map[string]interface{}
}{
protocol: string(inbounds[inbound_index].Protocol),
tag: inbounds[inbound_index].Tag,
client: c,
})
}
clients[client_index] = interface{}(c)
break
}
}
}
settings["clients"] = clients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return false, 0, err
}
inbounds[inbound_index].Settings = string(newSettings)
}
err = tx.Save(inbounds).Error
if err != nil {
return false, 0, err
}
err = tx.Save(traffics).Error
if err != nil {
return false, 0, err
}
if p != nil {
err1 = s.xrayApi.Init(p.GetAPIPort())
if err1 != nil {
return true, int64(len(traffics)), nil
}
for _, clientToAdd := range clientsToAdd {
err1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client)
if err1 != nil {
needRestart = true
}
}
s.xrayApi.Close()
}
return needRestart, int64(len(traffics)), nil
}
func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) { func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) {
now := time.Now().Unix() * 1000 now := time.Now().Unix() * 1000
needRestart := false needRestart := false
@@ -874,6 +990,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
clientTraffic.Enable = true clientTraffic.Enable = true
clientTraffic.Up = 0 clientTraffic.Up = 0
clientTraffic.Down = 0 clientTraffic.Down = 0
clientTraffic.Reset = client.Reset
result := tx.Create(&clientTraffic) result := tx.Create(&clientTraffic)
err := result.Error err := result.Error
if err != nil { if err != nil {
@@ -882,16 +999,15 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
return nil return nil
} }
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error { func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
db := database.GetDB() result := tx.Model(xray.ClientTraffic{}).
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", email). Where("email = ?", email).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"enable": true, "enable": true,
"email": client.Email, "email": client.Email,
"total": client.TotalGB, "total": client.TotalGB,
"expiry_time": client.ExpiryTime}) "expiry_time": client.ExpiryTime,
"reset": client.Reset})
err := result.Error err := result.Error
if err != nil { if err != nil {
return err return err
@@ -1011,7 +1127,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
} }
}() }()
whereText := "inbound_id " whereText := "reset = 0 and inbound_id "
if id < 0 { if id < 0 {
whereText += "> ?" whereText += "> ?"
} else { } else {
@@ -1248,3 +1364,7 @@ func (s *InboundService) MigrateDB() {
s.MigrationRequirements() s.MigrationRequirements()
s.MigrationRemoveOrphanedTraffics() s.MigrationRemoveOrphanedTraffics()
} }
func (s *InboundService) GetOnlineClinets() []string {
return p.GetOnlineClients()
}

View File

@@ -193,7 +193,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.AppStats.Mem = rtm.Sys status.AppStats.Mem = rtm.Sys
status.AppStats.Threads = uint32(runtime.NumGoroutine()) status.AppStats.Threads = uint32(runtime.NumGoroutine())
status.CpuCount = runtime.NumCPU() status.CpuCount = runtime.NumCPU()
if p.IsRunning() { if p != nil && p.IsRunning() {
status.AppStats.Uptime = p.GetUptime() status.AppStats.Uptime = p.GetUptime()
} else { } else {
status.AppStats.Uptime = 0 status.AppStats.Uptime = 0

View File

@@ -61,7 +61,7 @@ type SettingService struct {
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
db := database.GetDB() db := database.GetDB()
settings := make([]*model.Setting, 0) settings := make([]*model.Setting, 0)
err := db.Model(model.Setting{}).Find(&settings).Error err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -75,10 +75,16 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
} }
} }
bot, err = tgbotapi.NewBotAPI(tgBottoken) for {
if err != nil { bot, err = tgbotapi.NewBotAPI(tgBottoken)
fmt.Println("Get tgbot's api error:", err) if err != nil {
return err fmt.Println("Get tgbot's api error:", err)
fmt.Println("Retrying after 10 secound...")
time.Sleep(10 * time.Second)
} else {
fmt.Println("Tgbot connected!")
break
}
} }
bot.Debug = false bot.Debug = false
@@ -207,8 +213,14 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName) t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
case "client_commands": case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpClientCommands")) t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpClientCommands"))
case "onlines":
t.onlineClients(callbackQuery.From.ID)
case "commands": case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpAdminCommands")) t.SendMsgToTgbot(callbackQuery.From.ID, t.I18nBot("tgbot.commands.helpAdminCommands"))
default:
if callbackQuery.Data[:7] == "client_" {
t.searchClient(callbackQuery.From.ID, callbackQuery.Data[7:])
}
} }
} }
@@ -233,6 +245,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
), ),
tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "commands"), tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.commands"), "commands"),
tgbotapi.NewInlineKeyboardButtonData(t.I18nBot("tgbot.buttons.onlines"), "onlines"),
), ),
) )
numericKeyboardClient := tgbotapi.NewInlineKeyboardMarkup( numericKeyboardClient := tgbotapi.NewInlineKeyboardMarkup(
@@ -429,77 +442,7 @@ func (t *Tgbot) getInboundUsages() string {
return info return info
} }
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) { func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic) string {
if len(tgUserName) == 0 {
msg := t.I18nBot("tgbot.answers.askToAddUser")
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil {
logger.Warning(err)
msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName)
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = t.I18nBot("tgbot.unlimited")
} else {
total = common.FormatTraffic((traffic.Total))
}
active := ""
if traffic.Enable {
active = t.I18nBot("tgbot.messages.yes")
} else {
active = t.I18nBot("tgbot.messages.no")
}
output := ""
output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output)
}
t.SendAnswer(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), false)
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg)
return
}
if traffic == nil {
msg := t.I18nBot("tgbot.noResult")
t.SendMsgToTgbot(chatId, msg)
return
}
expiryTime := "" expiryTime := ""
if traffic.ExpiryTime == 0 { if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited") expiryTime = t.I18nBot("tgbot.unlimited")
@@ -523,14 +466,70 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
active = t.I18nBot("tgbot.messages.no") active = t.I18nBot("tgbot.messages.no")
} }
status := t.I18nBot("offline")
if p.IsRunning() {
for _, online := range p.GetOnlineClients() {
if online == traffic.Email {
status = t.I18nBot("online")
break
}
}
}
output := "" output := ""
output += t.I18nBot("tgbot.messages.active", "Enable=="+active) output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
output += t.I18nBot("tgbot.messages.online", "Status=="+status)
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
return output
}
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
if len(tgUserName) == 0 {
msg := t.I18nBot("tgbot.answers.askToAddUser")
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil {
logger.Warning(err)
msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName)
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
output := t.clientInfoMsg(traffic)
t.SendMsgToTgbot(chatId, output)
}
t.SendAnswer(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), false)
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg)
return
}
if traffic == nil {
msg := t.I18nBot("tgbot.noResult")
t.SendMsgToTgbot(chatId, msg)
return
}
output := t.clientInfoMsg(traffic)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -563,37 +562,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
t.SendMsgToTgbot(chatId, info) t.SendMsgToTgbot(chatId, info)
for _, traffic := range inbound.ClientStats { for _, traffic := range inbound.ClientStats {
expiryTime := "" output := t.clientInfoMsg(&traffic)
if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = t.I18nBot("tgbot.unlimited")
} else {
total = common.FormatTraffic((traffic.Total))
}
active := ""
if traffic.Enable {
active = t.I18nBot("tgbot.messages.yes")
} else {
active = t.I18nBot("tgbot.messages.no")
}
output := ""
output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
} }
@@ -613,37 +582,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
return return
} }
expiryTime := "" output := t.clientInfoMsg(traffic)
if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = t.I18nBot("tgbot.unlimited")
} else {
total = common.FormatTraffic((traffic.Total))
}
active := ""
if traffic.Enable {
active = t.I18nBot("tgbot.messages.yes")
} else {
active = t.I18nBot("tgbot.messages.no")
}
output := ""
output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output)
} }
@@ -725,35 +664,7 @@ func (t *Tgbot) getExhausted() string {
output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.clients")) output += t.I18nBot("tgbot.messages.exhaustedMsg", "Type=="+t.I18nBot("tgbot.clients"))
for _, traffic := range exhaustedClients { for _, traffic := range exhaustedClients {
expiryTime := "" output += t.clientInfoMsg(&traffic)
if traffic.ExpiryTime == 0 {
expiryTime = t.I18nBot("tgbot.unlimited")
} else if traffic.ExpiryTime < 0 {
expiryTime += fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = t.I18nBot("tgbot.unlimited")
} else {
total = common.FormatTraffic((traffic.Total))
}
active := ""
if traffic.Enable {
active = t.I18nBot("tgbot.messages.yes")
} else {
active = t.I18nBot("tgbot.messages.no")
}
output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
output += "\r\n \r\n" output += "\r\n \r\n"
} }
} }
@@ -761,6 +672,25 @@ func (t *Tgbot) getExhausted() string {
return output return output
} }
func (t *Tgbot) onlineClients(chatId int64) {
if !p.IsRunning() {
return
}
onlines := p.GetOnlineClients()
output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(len(onlines)))
if len(onlines) > 0 {
keyboard := tgbotapi.NewInlineKeyboardMarkup()
for index, online := range onlines {
keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d: %s\r\n", index+1, online), "client_"+online)))
}
t.SendMsgToTgbot(chatId, output, keyboard)
} else {
t.SendMsgToTgbot(chatId, output)
}
}
func (t *Tgbot) sendBackup(chatId int64) { func (t *Tgbot) sendBackup(chatId int64) {
if !t.IsRunning() { if !t.IsRunning() {
return return

View File

@@ -0,0 +1,28 @@
package service
import (
_ "embed"
"encoding/json"
"x-ui/util/common"
"x-ui/xray"
)
type XraySettingService struct {
SettingService
}
func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
if err := s.CheckXrayConfig(newXraySettings); err != nil {
return err
}
return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings)
}
func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
xrayConfig := &xray.Config{}
err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig)
if err != nil {
return common.NewError("xray template config invalid:", err)
}
return nil
}

View File

@@ -12,7 +12,7 @@
"protocol" = "Protocol" "protocol" = "Protocol"
"search" = "Search" "search" = "Search"
"filter" = "Filter" "filter" = "Filter"
"loading" = "Loading" "loading" = "Loading..."
"second" = "Second" "second" = "Second"
"minute" = "Minute" "minute" = "Minute"
"hour" = "Hour" "hour" = "Hour"
@@ -37,7 +37,9 @@
"enabled" = "Enabled" "enabled" = "Enabled"
"disabled" = "Disabled" "disabled" = "Disabled"
"depleted" = "Depleted" "depleted" = "Depleted"
"depletingSoon" = "Depleting soon" "depletingSoon" = "Depleting"
"offline" = "Offline"
"online" = "Online"
"domainName" = "Domain name" "domainName" = "Domain name"
"monitor" = "Listen IP" "monitor" = "Listen IP"
"certificate" = "Certificate" "certificate" = "Certificate"
@@ -55,6 +57,7 @@
"dashboard" = "System Status" "dashboard" = "System Status"
"inbounds" = "Inbounds" "inbounds" = "Inbounds"
"settings" = "Panel Settings" "settings" = "Panel Settings"
"xray" = "Xray Settings"
"logout" = "Logout" "logout" = "Logout"
"link" = "Other" "link" = "Other"
@@ -121,6 +124,8 @@
"modifyInbound" = "Modify Inbound" "modifyInbound" = "Modify Inbound"
"deleteInbound" = "Delete Inbound" "deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?" "deleteInboundContent" = "Are you sure you want to delete inbound?"
"deleteClient" = "Delete Client"
"deleteClientContent" = "Are you sure you want to delete client?"
"resetTrafficContent" = "Are you sure you want to reset traffic?" "resetTrafficContent" = "Are you sure you want to reset traffic?"
"copyLink" = "Copy Link" "copyLink" = "Copy Link"
"address" = "Address" "address" = "Address"
@@ -132,8 +137,8 @@
"totalFlow" = "Total Flow" "totalFlow" = "Total Flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire" "leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "No special requirements to keep the default" "noRecommendKeepDefault" = "No special requirements to keep the default"
"certificatePath" = "Certificate File Path" "certificatePath" = "File Path"
"certificateContent" = "Certificate File Content" "certificateContent" = "File Content"
"publicKeyPath" = "Public Key Path" "publicKeyPath" = "Public Key Path"
"publicKeyContent" = "Public Key Content" "publicKeyContent" = "Public Key Content"
"keyPath" = "Private Key Path" "keyPath" = "Private Key Path"
@@ -160,8 +165,9 @@
"email" = "Email" "email" = "Email"
"emailDesc" = "Please provide a unique email address." "emailDesc" = "Please provide a unique email address."
"setDefaultCert" = "Set cert from panel" "setDefaultCert" = "Set cert from panel"
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )" "telegramDesc" = "Use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations" "subscriptionDesc" = "You can find your sub link on Details, also you can use the same name for several configurations"
"info" = "Info"
[pages.client] [pages.client]
"add" = "Add Client" "add" = "Add Client"
@@ -178,6 +184,8 @@
"delayedStart" = "Start after first use" "delayedStart" = "Start after first use"
"expireDays" = "Expire days" "expireDays" = "Expire days"
"days" = "day(s)" "days" = "day(s)"
"renew" = "Auto renew"
"renewDesc" = "Auto renew days after expiration. 0 = disable"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "Obtain" "obtain" = "Obtain"
@@ -208,7 +216,6 @@
"resetDefaultConfig" = "Reset to default config" "resetDefaultConfig" = "Reset to default config"
"panelConfig" = "Panel Configurations" "panelConfig" = "Panel Configurations"
"userSettings" = "User Settings" "userSettings" = "User Settings"
"xrayConfiguration" = "Xray Configurations"
"TGBotSettings" = "Telegram Bot Settings" "TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP" "panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs." "panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
@@ -272,8 +279,8 @@
"subShowInfo" = "Show usage info" "subShowInfo" = "Show usage info"
"subShowInfoDesc" = "Show remianed traffic and date after config name" "subShowInfoDesc" = "Show remianed traffic and date after config name"
[pages.settings.templates] [pages.xray]
"title" = "Templates" "title" = "Xray Settings"
"basicTemplate" = "Basic Template" "basicTemplate" = "Basic Template"
"advancedTemplate" = "Advanced Template" "advancedTemplate" = "Advanced Template"
"completeTemplate" = "Complete Template" "completeTemplate" = "Complete Template"
@@ -287,54 +294,54 @@
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains." "directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
"ipv4Configs" = "IPv4 Configs" "ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4." "ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
"xrayConfigTemplate" = "Xray Configuration Template" "Template" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template." "TemplateDesc" = "Generate the final Xray configuration file based on this template."
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol" "FreedomStrategy" = "Configure Strategy for Freedom Protocol"
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol." "FreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy" "RoutingStrategy" = "Configure Domains Routing Strategy"
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving." "RoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
"xrayConfigTorrent" = "Ban BitTorrent Usage" "Torrent" = "Ban BitTorrent Usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users." "TorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect" "PrivateIp" = "Ban Private IP Ranges to Connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges." "PrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
"xrayConfigAds" = "Block Ads" "Ads" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block ads" "AdsDesc" = "Change the configuration template to block ads"
"xrayConfigFamily" = "Enable Family-Friendly Configuration" "Family" = "Enable Family-Friendly Configuration"
"xrayConfigFamilyDesc" = "Avoid connecting to unsafe websites for family protection." "FamilyDesc" = "Avoid connecting to unsafe websites for family protection."
"xrayConfigIRIp" = "Disable connection to Iran IP ranges" "IRIp" = "Disable connection to Iran IP ranges"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges." "IRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
"xrayConfigIRDomain" = "Disable connection to Iran domains" "IRDomain" = "Disable connection to Iran domains"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains." "IRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
"xrayConfigChinaIp" = "Disable connection to China IP ranges" "ChinaIp" = "Disable connection to China IP ranges"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges." "ChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
"xrayConfigChinaDomain" = "Disable connection to China domains" "ChinaDomain" = "Disable connection to China domains"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains." "ChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges" "RussiaIp" = "Disable connection to Russia IP ranges"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges." "RussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
"xrayConfigRussiaDomain" = "Disable connection to Russia domains" "RussiaDomain" = "Disable connection to Russia domains"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains." "RussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges" "DirectIRIp" = "Direct connection to Iran IP ranges"
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges." "DirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains" "DirectIRDomain" = "Direct connection to Iran domains"
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains." "DirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges" "DirectChinaIp" = "Direct connection to China IP ranges"
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges." "DirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
"xrayConfigDirectChinaDomain" = "Direct connection to China domains" "DirectChinaDomain" = "Direct connection to China domains"
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains." "DirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges" "DirectRussiaIp" = "Direct connection to Russia IP ranges"
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges." "DirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains" "DirectRussiaDomain" = "Direct connection to Russia domains"
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains." "DirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
"xrayConfigGoogleIPv4" = "Use IPv4 for Google" "GoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4." "GoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix" "NetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4." "NetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
"xrayConfigInbounds" = "Configuration of Inbounds" "Inbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients." "InboundsDesc" = "Change the configuration template to accept specific clients."
"xrayConfigOutbounds" = "Configuration of Outbounds" "Outbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server." "OutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
"xrayConfigRoutings" = "Configuration of routing rules" "Routings" = "Configuration of routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server." "RoutingsDesc" = "Change the configuration template to define routing rules for this server."
"manualLists" = "Manual Lists" "manualLists" = "Manual Lists"
"manualListsDesc" = "Please use the JSON array format." "manualListsDesc" = "Please use the JSON array format."
"manualBlockedIPs" = "List of Blocked IPs" "manualBlockedIPs" = "List of Blocked IPs"
@@ -398,12 +405,14 @@
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n" "expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n" "expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
"active" = "💡 Active: {{ .Enable }}\r\n" "active" = "💡 Active: {{ .Enable }}\r\n"
"online" = "🌐 Connection status: {{ .Status }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload↑: {{ .Upload }}\r\n" "upload" = "🔼 Upload↑: {{ .Upload }}\r\n"
"download" = "🔽 Download↓: {{ .Download }}\r\n" "download" = "🔽 Download↓: {{ .Download }}\r\n"
"total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n" "total" = "🔄 Total: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n" "exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n" "exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n"
"onlinesCount" = "🌐 Online clients count: {{ .Count }}\r\n"
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n" "disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n" "backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
@@ -416,6 +425,7 @@
"getInbounds" = "Get Inbounds" "getInbounds" = "Get Inbounds"
"depleteSoon" = "Deplete soon" "depleteSoon" = "Deplete soon"
"clientUsage" = "Get Usage" "clientUsage" = "Get Usage"
"onlines" = "Online Clients"
"commands" = "Commands" "commands" = "Commands"
[tgbot.answers] [tgbot.answers]

View File

@@ -12,7 +12,7 @@
"protocol" = "پروتکل" "protocol" = "پروتکل"
"search" = "جستجو" "search" = "جستجو"
"filter" = "فیلتر" "filter" = "فیلتر"
"loading" = "در حال بروزرسانی.." "loading" = "در حال بروزرسانی..."
"second" = "ثانیه" "second" = "ثانیه"
"minute" = "دقیقه" "minute" = "دقیقه"
"hour" = "ساعت" "hour" = "ساعت"
@@ -38,6 +38,8 @@
"disabled" = "غیرفعال" "disabled" = "غیرفعال"
"depleted" = "منقضی" "depleted" = "منقضی"
"depletingSoon" = "در حال انقضا" "depletingSoon" = "در حال انقضا"
"offline" = "آفلاین"
"online" = "آنلاین"
"domainName" = "آدرس دامنه" "domainName" = "آدرس دامنه"
"monitor" = "آی پی اتصال" "monitor" = "آی پی اتصال"
"certificate" = "گواهی دیجیتال" "certificate" = "گواهی دیجیتال"
@@ -55,11 +57,12 @@
"dashboard" = "وضعیت سیستم" "dashboard" = "وضعیت سیستم"
"inbounds" = "سرویس ها" "inbounds" = "سرویس ها"
"settings" = "تنظیمات پنل" "settings" = "تنظیمات پنل"
"xray" = "الگوی ایکس‌ری"
"logout" = "خروج" "logout" = "خروج"
"link" = "دیگر" "link" = "دیگر"
[pages.login] [pages.login]
"title" = "ورود به سیستم X-UI" "title" = "ورود به سیستم"
"loginAgain" = "مدت زمان استفاده به اتمام رسیده ، لطفا دوباره وارد شوید" "loginAgain" = "مدت زمان استفاده به اتمام رسیده ، لطفا دوباره وارد شوید"
[pages.login.toasts] [pages.login.toasts]
@@ -121,6 +124,8 @@
"modifyInbound" = "ویرایش سرویس" "modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس" "deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟" "deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
"deleteClient" = "حذف کاربر"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید ؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید ؟" "resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید ؟"
"copyLink" = "کپی لینک" "copyLink" = "کپی لینک"
"address" = "آدرس" "address" = "آدرس"
@@ -132,8 +137,8 @@
"totalFlow" = "کل ترافیک" "totalFlow" = "کل ترافیک"
"leaveBlankToNeverExpire" = "خالی بگذارید تا هرگز منقضی نشود" "leaveBlankToNeverExpire" = "خالی بگذارید تا هرگز منقضی نشود"
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود" "noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
"certificatePath" = "مسیر فایل گواهی" "certificatePath" = "مسیر فایل"
"certificateContent" = "محتوای فایل گواهی" "certificateContent" = "محتوای فایل"
"publicKeyPath" = "مسیر کلید عمومی" "publicKeyPath" = "مسیر کلید عمومی"
"publicKeyContent" = "محتوای کلید عمومی" "publicKeyContent" = "محتوای کلید عمومی"
"keyPath" = "مسیر کلید خصوصی" "keyPath" = "مسیر کلید خصوصی"
@@ -161,6 +166,7 @@
"setDefaultCert" = "استفاده از گواهی پنل" "setDefaultCert" = "استفاده از گواهی پنل"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)" "telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
"info" = "اطلاعات"
[pages.client] [pages.client]
"add" = "کاربر جدید" "add" = "کاربر جدید"
@@ -177,6 +183,8 @@
"delayedStart" = "شروع بعد از اولین استفاده" "delayedStart" = "شروع بعد از اولین استفاده"
"expireDays" = "روزهای اعتبار" "expireDays" = "روزهای اعتبار"
"days" = "(روز)" "days" = "(روز)"
"renew" = "تمدید خودکار"
"renewDesc" = "روزهای تمدید خودکار پس از انقضا. 0 = غیرفعال"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "Obtain" "obtain" = "Obtain"
@@ -207,7 +215,6 @@
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض" "resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelConfig" = "تنظیمات پنل" "panelConfig" = "تنظیمات پنل"
"userSettings" = "تنظیمات مدیر" "userSettings" = "تنظیمات مدیر"
"xrayConfiguration" = "تنظیمات Xray"
"TGBotSettings" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
@@ -271,8 +278,8 @@
"subShowInfo" = "نمایش اطلاعات مصرف" "subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد" "subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
[pages.settings.templates] [pages.xray]
"title" = "الگوها" "title" = "تنظیمات Xray"
"basicTemplate" = "بخش الگو پایه" "basicTemplate" = "بخش الگو پایه"
"advancedTemplate" = "بخش الگو پیشرفته" "advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل" "completeTemplate" = "بخش الگو کامل"
@@ -286,54 +293,54 @@
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند" "directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
"ipv4Configs" = "تنظیمات برای IPv4" "ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود" "ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری" "Template" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!" "TemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم" "FreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم" "FreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی" "RoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه" "RoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت" "Torrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد" "TorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی" "PrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد" "PrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
"xrayConfigAds" = "مسدود کردن تبلیغات" "Ads" = "مسدود کردن تبلیغات"
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد" "AdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
"xrayConfigFamily" = "فعال کردن حالت خانواده" "Family" = "فعال کردن حالت خانواده"
"xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن" "FamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران" "IRIp" = "جلوگیری از اتصال آیپی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد" "IRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران" "IRDomain" = "جلوگیری از اتصال دامنه های ایران"
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد" "IRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین" "ChinaIp" = "جلوگیری از اتصال آیپی های چین"
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد" "ChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین" "ChinaDomain" = "جلوگیری از اتصال دامنه های چین"
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد" "ChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه" "RussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد" "RussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه" "RussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد" "RussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران" "DirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد" "DirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران" "DirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد" "DirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین" "DirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد" "DirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین" "DirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد" "DirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه" "DirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد" "DirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه" "DirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد" "DirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل" "GoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند" "GoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس" "NetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند" "NetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
"xrayConfigInbounds" = "تنظیمات ورودی" "Inbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید" "InboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
"xrayConfigOutbounds" = "تنظیمات خروجی" "Outbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید" "OutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی" "Routings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید" "RoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
"manualLists" = "لیست های دستی" "manualLists" = "لیست های دستی"
"manualListsDesc" = "فرمت: JSON Array" "manualListsDesc" = "فرمت: JSON Array"
"manualBlockedIPs" = "لیست آی‌پی های مسدود شده" "manualBlockedIPs" = "لیست آی‌پی های مسدود شده"
@@ -397,12 +404,14 @@
"expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n" "expire" = "📅 تاریخ انقضا: {{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n" "expireIn" = "📅 باقیمانده از انقضا: {{ .Time }}\r\n \r\n"
"active" = "💡 فعال: {{ .Enable }}\r\n" "active" = "💡 فعال: {{ .Enable }}\r\n"
"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n" "email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n" "upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n" "download" = "🔽 دانلود↓: {{ .Download }}\r\n"
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n" "total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n" "exhaustedMsg" = "🚨 {{ .Type }} به اتمام رسیده است:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n" "exhaustedCount" = "🚨 تعداد {{ .Type }} به اتمام رسیده:\r\n"
"onlinesCount" = "🌐 تعداد کاربران آنلاین: {{ .Count }}\r\n"
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n" "disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 به زودی به پایان خواهد رسید: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n" "backupTime" = "🗄 زمان پشتیبان‌گیری: {{ .Time }}\r\n"
@@ -415,6 +424,7 @@
"getInbounds" = "دریافت ورودی‌ها" "getInbounds" = "دریافت ورودی‌ها"
"depleteSoon" = "به زودی به پایان خواهد رسید" "depleteSoon" = "به زودی به پایان خواهد رسید"
"clientUsage" = "دریافت آمار کاربر" "clientUsage" = "دریافت آمار کاربر"
"onlines" = "کاربران آنلاین"
"commands" = "دستورات" "commands" = "دستورات"
[tgbot.answers] [tgbot.answers]

View File

@@ -12,7 +12,7 @@
"protocol" = "Протокол" "protocol" = "Протокол"
"search" = "Поиск" "search" = "Поиск"
"filter" = "Фильтр" "filter" = "Фильтр"
"loading" = "Загрузка" "loading" = "Загрузка..."
"second" = "Секунда" "second" = "Секунда"
"minute" = "Минута" "minute" = "Минута"
"hour" = "Час" "hour" = "Час"
@@ -38,6 +38,8 @@
"disabled" = "Отключено" "disabled" = "Отключено"
"depleted" = "Отключены" "depleted" = "Отключены"
"depletingSoon" = "Почти отключены" "depletingSoon" = "Почти отключены"
"offline" = "Офлайн"
"online" = "Онлайн"
"domainName" = "Домен" "domainName" = "Домен"
"monitor" = "Прослушиваемый IP" "monitor" = "Прослушиваемый IP"
"certificate" = "Сертификат" "certificate" = "Сертификат"
@@ -55,6 +57,7 @@
"dashboard" = "Статус системы" "dashboard" = "Статус системы"
"inbounds" = "Подключения" "inbounds" = "Подключения"
"settings" = "Настройки" "settings" = "Настройки"
"xray" = "Xray Настройки"
"logout" = "Выйти" "logout" = "Выйти"
"link" = "Другое" "link" = "Другое"
@@ -121,6 +124,8 @@
"modifyInbound" = "Изменить данные" "modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить подключение" "deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?" "deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
"deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Подтвердите обнуление трафика?" "resetTrafficContent" = "Подтвердите обнуление трафика?"
"copyLink" = "Копировать ключ" "copyLink" = "Копировать ключ"
"address" = "Адрес" "address" = "Адрес"
@@ -132,8 +137,8 @@
"totalFlow" = "Общий расход" "totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы сделать бессрочно" "leaveBlankToNeverExpire" = "Оставьте пустым, чтобы сделать бессрочно"
"noRecommendKeepDefault" = "Нет особых требований для сохранения настроек по умолчанию" "noRecommendKeepDefault" = "Нет особых требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата" "certificatePath" = "Путь файла"
"certificateContent" = "Содержимое файла сертификата" "certificateContent" = "Содержимое файла"
"publicKeyPath" = "Путь к публичному ключу" "publicKeyPath" = "Путь к публичному ключу"
"publicKeyContent" = "Содержимое публичного ключа" "publicKeyContent" = "Содержимое публичного ключа"
"keyPath" = "Путь к приватному ключу" "keyPath" = "Путь к приватному ключу"
@@ -162,6 +167,7 @@
"setDefaultCert" = "Установить сертификат с панели" "setDefaultCert" = "Установить сертификат с панели"
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)" "telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов" "subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
"info" = "Информация"
[pages.client] [pages.client]
"add" = "Добавить клиента" "add" = "Добавить клиента"
@@ -178,6 +184,8 @@
"delayedStart" = "Начать со времени первого подключения" "delayedStart" = "Начать со времени первого подключения"
"expireDays" = "Срок действия" "expireDays" = "Срок действия"
"days" = "дней" "days" = "дней"
"renew" = "Автопродление"
"renewDesc" = "Автоматическое продление через несколько дней после истечения срока действия. 0 = отключить"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "Получить" "obtain" = "Получить"
@@ -208,7 +216,6 @@
"resetDefaultConfig" = "Сбросить всё по-умолчанию" "resetDefaultConfig" = "Сбросить всё по-умолчанию"
"panelConfig" = "Настройки панели" "panelConfig" = "Настройки панели"
"userSettings" = "Настройки безопасности" "userSettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота" "TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-адрес прослушивания панели" "panelListeningIP" = "IP-адрес прослушивания панели"
"panelListeningIPDesc" = "Оставьте пустым, чтобы прослушивать все IP-адреса." "panelListeningIPDesc" = "Оставьте пустым, чтобы прослушивать все IP-адреса."
@@ -272,8 +279,8 @@
"subShowInfo" = "Показать информацию об использовании" "subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации" "subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
[pages.settings.templates] [pages.xray]
"title" = "Шаблоны" "title" = "Xray Настройки"
"basicTemplate" = "Базовые шаблоны" "basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны" "advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Итоговый шаблон" "completeTemplate" = "Итоговый шаблон"
@@ -287,54 +294,54 @@
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны." "directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны."
"ipv4Configs" = "Настройки IPv4" "ipv4Configs" = "Настройки IPv4"
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4" "ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
"xrayConfigTemplate" = "Шаблон конфигурации Xray" "Template" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона." "TemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона."
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom" "FreedomStrategy" = "Настроить стратегию протокола Freedom"
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom" "FreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации" "RoutingStrategy" = "Настроить доменную стратегию маршрутизации"
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS" "RoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
"xrayConfigTorrent" = "Запретить использование BitTorrent" "Torrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent." "TorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent."
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения" "PrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов." "PrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов."
"xrayConfigAds" = "Блокировка рекламы" "Ads" = "Блокировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу." "AdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу."
"xrayConfigFamily" = "Включить семейную конфигурацию" "Family" = "Включить семейную конфигурацию"
"xrayConfigFamilyDesc" = "Избегать подключения к небезопасным веб-сайтам для всей семьи" "FamilyDesc" = "Избегать подключения к небезопасным веб-сайтам для всей семьи"
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана" "IRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана." "IRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана."
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана" "IRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана." "IRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана."
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая" "ChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая." "ChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая."
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая" "ChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая." "ChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая."
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России" "RussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России." "RussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России."
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России" "RussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России." "RussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России."
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана" "DirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана" "DirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана" "DirectIRDomain" = "Прямое подключение к доменам Ирана"
"xrayConfigDirectIRDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Ирана" "DirectIRDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Ирана"
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая" "DirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая"
"xrayConfigDirectChinaIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Китая" "DirectChinaIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Китая"
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая" "DirectChinaDomain" = "Прямое подключение к доменам Китая"
"xrayConfigDirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая" "DirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая"
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России" "DirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
"xrayConfigDirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России" "DirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России"
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России" "DirectRussiaDomain" = "Прямое подключение к доменам России"
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России" "DirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google" "GoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4." "GoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4."
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix" "NetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4." "NetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4."
"xrayConfigInbounds" = "Конфигурация подключений" "Inbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей." "InboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей."
"xrayConfigOutbounds" = "Конфигурация исходящих" "Outbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера." "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера."
"xrayConfigRoutings" = "Настройка правил маршрутизации" "Routings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера." "RoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера."
"manualLists" = "Пользовательские списки" "manualLists" = "Пользовательские списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON" "manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP-адресов" "manualBlockedIPs" = "Список заблокированных IP-адресов"
@@ -398,12 +405,14 @@
"expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n" "expire" = "📅 Дата окончания: {{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n" "expireIn" = "📅 Окончание через: {{ .Time }}\r\n \r\n"
"active" = "💡 Активен: {{ .Enable }}\r\n" "active" = "💡 Активен: {{ .Enable }}\r\n"
"online" = "🌐 Статус соединения: {{ .Status }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n" "upload" = "🔼 Загрузка↑: {{ .Upload }}\r\n"
"download" = "🔽 Скачивание↓: {{ .Download }}\r\n" "download" = "🔽 Скачивание↓: {{ .Download }}\r\n"
"total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n" "total" = "🔄 Всего: {{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 Истекли {{ .Type }}:\r\n" "exhaustedMsg" = "🚨 Истекли {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество истекших {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Количество истекших {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Количество онлайн-клиентов: {{ .Count }}\r\n"
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n" "disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Скоро отключатся: {{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 Скоро отключатся: {{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n" "backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
@@ -416,6 +425,7 @@
"getInbounds" = "Получить список подключений" "getInbounds" = "Получить список подключений"
"depleteSoon" = "Скоро отключатся" "depleteSoon" = "Скоро отключатся"
"clientUsage" = "Получить статистику" "clientUsage" = "Получить статистику"
"onlines" = "Онлайн-клиенты"
"commands" = "Команды" "commands" = "Команды"
[tgbot.answers] [tgbot.answers]

View File

@@ -12,7 +12,7 @@
"protocol" = "协议" "protocol" = "协议"
"search" = "搜尋" "search" = "搜尋"
"filter" = "过滤器" "filter" = "过滤器"
"loading" = "加载中" "loading" = "加载中..."
"second" = "秒" "second" = "秒"
"minute" = "分钟" "minute" = "分钟"
"hour" = "小时" "hour" = "小时"
@@ -38,6 +38,8 @@
"disabled" = "关闭" "disabled" = "关闭"
"depleted" = "耗尽" "depleted" = "耗尽"
"depletingSoon" = "即将耗尽" "depletingSoon" = "即将耗尽"
"offline" = "离线"
"online" = "在线"
"domainName" = "域名" "domainName" = "域名"
"monitor" = "监听" "monitor" = "监听"
"certificate" = "证书" "certificate" = "证书"
@@ -55,6 +57,7 @@
"dashboard" = "系统状态" "dashboard" = "系统状态"
"inbounds" = "入站列表" "inbounds" = "入站列表"
"settings" = "面板设置" "settings" = "面板设置"
"xray" = "Xray 设置"
"logout" = "退出登录" "logout" = "退出登录"
"link" = "其他" "link" = "其他"
@@ -121,6 +124,8 @@
"modifyInbound" = "修改入站" "modifyInbound" = "修改入站"
"deleteInbound" = "删除入站" "deleteInbound" = "删除入站"
"deleteInboundContent" = "确定要删除入站吗?" "deleteInboundContent" = "确定要删除入站吗?"
"deleteClient" = "删除客户端"
"deleteClientContent" = "您确定要删除客户端吗?"
"resetTrafficContent" = "确定要重置流量吗?" "resetTrafficContent" = "确定要重置流量吗?"
"copyLink" = "复制链接" "copyLink" = "复制链接"
"address" = "地址" "address" = "地址"
@@ -132,8 +137,8 @@
"totalFlow" = "总流量" "totalFlow" = "总流量"
"leaveBlankToNeverExpire" = "留空则永不到期" "leaveBlankToNeverExpire" = "留空则永不到期"
"noRecommendKeepDefault" = "没有特殊需求保持默认即可" "noRecommendKeepDefault" = "没有特殊需求保持默认即可"
"certificatePath" = "证书文件路径" "certificatePath" = "文件路径"
"certificateContent" = "证书文件内容" "certificateContent" = "文件内容"
"publicKeyPath" = "公钥文件路径" "publicKeyPath" = "公钥文件路径"
"publicKeyContent" = "公钥内容" "publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径" "keyPath" = "密钥文件路径"
@@ -162,6 +167,7 @@
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)" "telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
"info" = "信息"
[pages.client] [pages.client]
"add" = "添加客户端" "add" = "添加客户端"
@@ -178,6 +184,8 @@
"delayedStart" = "首次使用后开始" "delayedStart" = "首次使用后开始"
"expireDays" = "过期天数" "expireDays" = "过期天数"
"days" = "天" "days" = "天"
"renew" = "自动续订"
"renewDesc" = "过期后自动续订。0 = 禁用"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "获取" "obtain" = "获取"
@@ -208,7 +216,6 @@
"resetDefaultConfig" = "重置为默认配置" "resetDefaultConfig" = "重置为默认配置"
"panelConfig" = "面板配置" "panelConfig" = "面板配置"
"userSettings" = "用户设置" "userSettings" = "用户设置"
"xrayConfiguration" = "Xray 相关设置"
"TGBotSettings" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP" "panelListeningIPDesc" = "默认留空监听所有 IP"
@@ -273,7 +280,7 @@
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期" "subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
[pages.settings.templates] [pages.settings.templates]
"title" = "模板" "title" = "Xray 设置"
"basicTemplate" = "基本模板" "basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件" "advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板" "completeTemplate" = "Xray 配置的完整模板"
@@ -287,54 +294,54 @@
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。" "directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
"ipv4Configs" = "IPv4 配置" "ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域" "ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
"xrayConfigTemplate" = "Xray 配置模板" "Template" = "Xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件重新启动面板生成效率" "TemplateDesc" = "以该模型为基础生成最终的Xray配置文件重新启动面板生成效率"
"xrayConfigFreedomStrategy" = "配置自由协议的策略" "FreedomStrategy" = "配置自由协议的策略"
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略" "FreedomStrategyDesc" = "在自由协议中设置网络输出策略"
"xrayConfigRoutingStrategy" = "配置路由域策略" "RoutingStrategy" = "配置路由域策略"
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略" "RoutingStrategyDesc" = "设置DNS解析的整体路由策略"
"xrayConfigTorrent" = "禁止使用 bittorrent" "Torrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent" "TorrentDesc" = "更改配置模板避免用户使用bittorrent"
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接" "PrivateIp" = "禁止私人 IP 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围" "PrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
"xrayConfigAds" = "屏蔽广告" "Ads" = "屏蔽广告"
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告" "AdsDesc" = "修改配置模板屏蔽广告"
"xrayConfigFamily" = "启用家庭友好配置" "Family" = "启用家庭友好配置"
"xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站" "FamilyDesc" = "避免为家人连接到不安全的网站"
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接" "IRIp" = "禁止伊朗 IP 范围连接"
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段" "IRIpDesc" = "修改配置模板避免连接伊朗IP段"
"xrayConfigIRDomain" = "禁止伊朗域连接" "IRDomain" = "禁止伊朗域连接"
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名" "IRDomainDesc" = "更改配置模板避免连接伊朗域名"
"xrayConfigChinaIp" = "禁止中国 IP 范围连接" "ChinaIp" = "禁止中国 IP 范围连接"
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段" "ChinaIpDesc" = "修改配置模板避免连接中国IP段"
"xrayConfigChinaDomain" = "禁止中国域名连接" "ChinaDomain" = "禁止中国域名连接"
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域" "ChinaDomainDesc" = "更改配置模板避免连接中国域"
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接" "RussiaIp" = "禁止俄罗斯 IP 范围连接"
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围" "RussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接" "RussiaDomain" = "禁止俄罗斯域连接"
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域" "RussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
"xrayConfigDirectIRIp" = "直接连接到伊朗 IP 范围" "DirectIRIp" = "直接连接到伊朗 IP 范围"
"xrayConfigDirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板" "DirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
"xrayConfigDirectIRDomain" = "直接连接到伊朗域" "DirectIRDomain" = "直接连接到伊朗域"
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板" "DirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
"xrayConfigDirectChinaIp" = "直连中国IP范围" "DirectChinaIp" = "直连中国IP范围"
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板" "DirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
"xrayConfigDirectChinaDomain" = "直连中国域名" "DirectChinaDomain" = "直连中国域名"
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板" "DirectChinaDomainDesc" = "修改中国域名直连配置模板"
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围" "DirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板" "DirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域" "DirectRussiaDomain" = "直接连接到俄罗斯域"
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板" "DirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4" "GoogleIPv4" = "为谷歌使用 IPv4"
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由" "GoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4" "NetflixIPv4" = "为 Netflix 使用 IPv4"
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由" "NetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
"xrayConfigInbounds" = "入站配置" "Inbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端" "InboundsDesc" = "更改配置模板接受特殊客户端"
"xrayConfigOutbounds" = "出站配置" "Outbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式" "OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"xrayConfigRoutings" = "路由规则配置" "Routings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则" "RoutingsDesc" = "更改配置模板为该服务器定义路由规则"
"manualLists" = "手动列表" "manualLists" = "手动列表"
"manualListsDesc" = "请使用 JSON 数组格式" "manualListsDesc" = "请使用 JSON 数组格式"
"manualBlockedIPs" = "被阻止的 IP 列表" "manualBlockedIPs" = "被阻止的 IP 列表"
@@ -398,12 +405,14 @@
"expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n" "expire" = "📅 过期日期:{{ .DateTime }}\r\n \r\n"
"expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n" "expireIn" = "📅 剩余时间:{{ .Time }}\r\n \r\n"
"active" = "💡 激活:{{ .Enable }}\r\n" "active" = "💡 激活:{{ .Enable }}\r\n"
"online" = "🌐 连接状态: {{ .Status }}\r\n"
"email" = "📧 邮箱:{{ .Email }}\r\n" "email" = "📧 邮箱:{{ .Email }}\r\n"
"upload" = "🔼 上传↑:{{ .Upload }}\r\n" "upload" = "🔼 上传↑:{{ .Upload }}\r\n"
"download" = "🔽 下载↓:{{ .Download }}\r\n" "download" = "🔽 下载↓:{{ .Download }}\r\n"
"total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n" "total" = "🔄 总计:{{ .UpDown }} / {{ .Total }}\r\n"
"exhaustedMsg" = "🚨 耗尽的{{ .Type }}\r\n" "exhaustedMsg" = "🚨 耗尽的{{ .Type }}\r\n"
"exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n" "exhaustedCount" = "🚨 耗尽的{{ .Type }}数量:\r\n"
"onlinesCount" = "🌐 Количество онлайн-клиентов: {{ .Count }}\r\n"
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n" "disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
"depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n" "depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n \r\n"
"backupTime" = "🗄 备份时间:{{ .Time }}\r\n" "backupTime" = "🗄 备份时间:{{ .Time }}\r\n"
@@ -416,6 +425,7 @@
"getInbounds" = "获取入站信息" "getInbounds" = "获取入站信息"
"depleteSoon" = "即将耗尽" "depleteSoon" = "即将耗尽"
"clientUsage" = "获取使用情况" "clientUsage" = "获取使用情况"
"onlineUsers" = "在线客户"
"commands" = "命令" "commands" = "命令"
[tgbot.answers] [tgbot.answers]

View File

@@ -354,7 +354,7 @@ func (s *Server) Start() (err error) {
isTgbotenabled, err := s.settingService.GetTgbotenabled() isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) { if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot() tgBot := s.tgbotService.NewTgbot()
tgBot.Start(i18nFS) go tgBot.Start(i18nFS)
} }
return nil return nil

View File

@@ -9,4 +9,5 @@ type ClientTraffic struct {
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"` Total int64 `json:"total" form:"total"`
Reset int `json:"reset" form:"reset" gorm:"default:0"`
} }

View File

@@ -59,6 +59,8 @@ type process struct {
version string version string
apiPort int apiPort int
onlineClients []string
config *Config config *Config
lines *queue.Queue lines *queue.Queue
exitErr error exitErr error
@@ -114,6 +116,14 @@ func (p *Process) GetConfig() *Config {
return p.config return p.config
} }
func (p *Process) GetOnlineClients() []string {
return p.onlineClients
}
func (p *Process) SetOnlineClients(users []string) {
p.onlineClients = users
}
func (p *Process) GetUptime() uint64 { func (p *Process) GetUptime() uint64 {
return uint64(time.Since(p.startTime).Seconds()) return uint64(time.Since(p.startTime).Seconds())
} }