Compare commits

...

127 Commits

Author SHA1 Message Date
MHSanaei
85d42ce94f bug fix - expiry Time 2023-05-17 18:57:34 +03:30
MHSanaei
2f3c3d0ed2 v1.5.0 2023-05-17 18:19:39 +03:30
MHSanaei
7debf96610 fix bug - mobile view 2023-05-17 18:17:12 +03:30
Ho3ein
d8b60c3cd5 Update docker.yml
always use the latest version :D
2023-05-17 16:34:44 +03:30
MHSanaei
f8eb548376 [feature] SpiderX for Reality
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 16:10:11 +03:30
MHSanaei
88fc4f81d4 [feature] filter inbound clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 15:57:47 +03:30
MHSanaei
911f2b0bb5 adguard Family protection 2023-05-17 03:05:36 +03:30
MHSanaei
2d16eabc6e [feature] interactive deplete soon
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 02:01:56 +03:30
MHSanaei
be50be75fe [sub] add more headers
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 01:07:35 +03:30
MHSanaei
70e7987df5 [feature] multi cert per inbound
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 00:55:19 +03:30
MHSanaei
8ceeb454ee random - trojan password 2023-05-17 00:09:32 +03:30
MHSanaei
f311bf1dbf update - ui 2023-05-16 23:40:27 +03:30
MHSanaei
837d7f77a1 update - xray configuration
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-16 23:39:59 +03:30
Ho3ein
d0213ce50b Merge pull request #452 from MHSanaei/dependabot/github_actions/actions/setup-go-4.0.1
Bump actions/setup-go from 4.0.0 to 4.0.1
2023-05-16 14:36:51 +03:30
dependabot[bot]
622e440366 Bump actions/setup-go from 4.0.0 to 4.0.1
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-16 10:04:13 +00:00
MHSanaei
1dc5452f1d security issue - CVE-2023-29401
Gin Web Framework does not properly sanitize filename parameter of Context.FileAttachment function

References
gin-gonic/gin#3555
gin-gonic/gin#3556
https://pkg.go.dev/vuln/GO-2023-1737
2023-05-15 15:59:27 +03:30
Ho3ein
a0daf2fae2 Merge pull request #438 from masoud-hidden/main
[tgbot] Some new changes in the bot
2023-05-14 23:58:54 +03:30
Masoud Hidden
a21bdc9396 [tgbot] Fix reply keyboard height 2023-05-14 22:55:01 +03:30
Masoud Hidden
4cf7f75749 [tgbot] Ability to remove telegram user from the client in the bot 2023-05-14 22:43:23 +03:30
Masoud Hidden
bcdac5aad6 [tgbot] Ability to select telegram user for client from bot 2023-05-14 22:07:49 +03:30
Masoud Hidden
3d5f851fce [tgbot] Changed to telego 2023-05-14 18:50:01 +03:30
Ho3ein
5487dc41cc Merge pull request #434 from hamid-gh98/main
[FIX] siderDrawer button functionality + [Update] redirect restart panel
2023-05-14 10:49:30 +03:30
Hamidreza Ghavami
5a908b9f58 FIX redirect after restart panel 2023-05-14 06:19:18 +04:30
Hamidreza Ghavami
61288db11e FIX siderDrawer button function 2023-05-14 06:08:49 +04:30
Hamidreza Ghavami
317f7fe9da FIX sideBar style 2023-05-14 06:08:34 +04:30
Ho3ein
7b5dd2d0ee Merge pull request #432 from hamid-gh98/main
[HOTFIX] Add basePath to Redirect Middleware
2023-05-14 01:51:02 +03:30
Hamidreza Ghavami
b1302c70fb Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-14 02:32:17 +04:30
Hamidreza Ghavami
addedb1adf HOTFIX redirect middleware to add basePath 2023-05-14 02:31:23 +04:30
MHSanaei
62bb42cfab v1.4.6 2023-05-14 01:10:51 +03:30
MHSanaei
f4be9f234a lang show 2023-05-14 01:09:31 +03:30
MHSanaei
947129a62a update UI - calendar 2023-05-14 01:08:29 +03:30
Ho3ein
66f0a13145 Merge pull request #431 from hamid-gh98/main
[HOTFIX] Redirect `/xui` to `/panel`
2023-05-14 01:07:17 +03:30
Hamidreza Ghavami
9626379731 Update README.md 2023-05-14 01:42:29 +04:30
Hamidreza Ghavami
c2c61cdd5b Add Redirect Middleware for Router 2023-05-14 01:42:08 +04:30
Hamidreza Ghavami
b5ae580d12 Update '/xui/API' to new path '/panel/api' 2023-05-14 01:41:18 +04:30
MHSanaei
63939244a4 v1.4.5 2023-05-13 22:31:13 +03:30
MHSanaei
213b693bd3 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-05-13 19:06:19 +03:30
MHSanaei
a289ef5d10 bug fixed - random user pass 2023-05-13 19:06:16 +03:30
Ho3ein
955eb8f142 Merge pull request #428 from LOVECHEN/main
Update docker-compose.yml
2023-05-13 18:43:53 +03:30
LOVECHEN
d396fb5d06 Update docker-compose.yml
Define your hostname to identify the host in telegram
2023-05-13 22:55:47 +08:00
Ho3ein
b5dd258074 Merge pull request #426 from hamid-gh98/main
FIX input bg color in login page
2023-05-13 18:15:39 +03:30
MHSanaei
c855a292cb random sub button 2023-05-13 17:22:13 +03:30
Hamidreza Ghavami
f2132c62e9 fix input style 2023-05-13 18:00:47 +04:30
Hamidreza Ghavami
94a3807353 fix input bg color in login page 2023-05-13 17:42:11 +04:30
MHSanaei
7cacfc074e remove duplicate random text gen
randomText by default
length set to 8
2023-05-13 15:42:46 +03:30
MHSanaei
9e8ac8a087 remove search Data files 2023-05-13 15:03:46 +03:30
MHSanaei
e64a9eeee6 random UUID 2023-05-13 14:51:07 +03:30
MHSanaei
a55a1a7102 fix 2023-05-13 13:53:17 +03:30
MHSanaei
46bc39c160 [bug] fix cloned inbound settings
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:33:13 +03:30
MHSanaei
2a182d8b9a [bug] fix login failure when tgbot is not active
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:31:46 +03:30
MHSanaei
77241c7fcf pruning some codes
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:29:10 +03:30
MHSanaei
fd6a85afd9 Set session max-age to default if defined zero
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:26:04 +03:30
MHSanaei
9a89d7bfab spin only in reload time
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:24:44 +03:30
MHSanaei
edd6b22109 remove duplicate remark assignments
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:21:27 +03:30
Ho3ein
5468069bef Merge pull request #420 from hamid-gh98/main
[fix] russia domains in settings and More....
2023-05-13 13:18:36 +03:30
Hamidreza Ghavami
0cce35784e FIX Login UI style 2023-05-13 00:52:12 +04:30
Hamidreza Ghavami
80c1e58ed5 Update README.md 2023-05-12 22:45:33 +04:30
Hamidreza Ghavami
b0871a6ef6 Change route path '/xui' to '/panel' 2023-05-12 22:45:32 +04:30
Hamidreza Ghavami
288374d5fa Update README.md 2023-05-12 22:45:32 +04:30
Hamidreza Ghavami
1f7c79c735 Add docker-compose.yml 2023-05-12 22:45:31 +04:30
Hamidreza Ghavami
251fd608df update translation 2023-05-12 22:45:30 +04:30
Hamidreza Ghavami
456941323b await secret status in login page 2023-05-12 22:45:30 +04:30
Hamidreza Ghavami
a6a77688dc Add block speedtest switch template 2023-05-12 22:45:29 +04:30
Hamidreza Ghavami
09cd2248dc fix show client name in QR modal 2023-05-12 22:45:28 +04:30
Hamidreza Ghavami
8143379645 Add copy button for sub link 2023-05-12 22:45:28 +04:30
Hamidreza Ghavami
5bd6baa055 Fix darkClass in ThemeSwitcher 2023-05-12 22:45:27 +04:30
Hamidreza Ghavami
41e9290574 Show client email in QR Modal 2023-05-12 22:45:26 +04:30
Hamidreza Ghavami
cf7d50617b add service function to search data files 2023-05-12 22:45:26 +04:30
Hamidreza Ghavami
95e006963c add searchDatafiles route 2023-05-12 22:45:25 +04:30
Hamidreza Ghavami
65588a4492 add check for geosite function 2023-05-12 22:45:24 +04:30
Hamidreza Ghavami
d39c7e4ae3 only get enabled inbounds and clients 2023-05-12 22:45:24 +04:30
Tara Rostami
3bec9ee273 Minor changes in UI (#415)
* Update custom.css

* Update setting.html

* Update settings.html

* Update antd.min.css

* Update antd.min.css

* Update settings.html

* Update custom.css

* Update custom.css

* Update antd.min.css

* Update setting.html

* Update custom.css

* Update custom.css

* Update antd.min.css
2023-05-12 09:13:08 +03:30
Tara Rostami
7b3628d33b Optimized Settings UI (#408)
* Update custom.css

* Update setting.html

* Update settings.html

* Update antd.min.css

* Update antd.min.css

* Update settings.html

* Update custom.css

* Update custom.css

* Update antd.min.css

* Update setting.html

* Update custom.css
2023-05-11 14:04:35 +03:30
Hossein Abaiyani
ad1aa5b2f9 Cleaner Docker file with much lighter base image (#387)
* updated dockerfile

* updated dockerfile

* Update Dockerfile

added platform

* added iran.dat

* added iran.dat

---------

Co-authored-by: Hossein Abaiyani <hossein.abaiyani@arvancloud.com>
2023-05-11 13:08:44 +03:30
Ho3ein
46ef8c503e Merge pull request #392 from MHSanaei/dependabot/go_modules/gorm.io/gorm-1.25.1
Bump gorm.io/gorm from 1.25.0 to 1.25.1
2023-05-09 19:57:19 +03:30
dependabot[bot]
721fec3b5a Bump gorm.io/gorm from 1.25.0 to 1.25.1
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.0 to 1.25.1.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.0...v1.25.1)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-09 10:03:47 +00:00
Ho3ein
30a5f66f26 Merge pull request #381 from hamid-gh98/main
[FIX] bug logout path + [UPDATE] login UI and more ...
2023-05-08 19:40:02 +03:30
Hamidreza Ghavami
bb6e6861ca fix style bg :D 2023-05-08 20:19:11 +04:30
Hamidreza Ghavami
4c0e391597 update theme-switch 2023-05-08 19:44:18 +04:30
Hamidreza Ghavami
43c1fc9aad Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-08 19:41:14 +04:30
Hamidreza Ghavami
7a48cbb191 fix style login ui 2023-05-08 19:38:36 +04:30
Hamidreza Ghavami
004d69392b fix use password component 2023-05-08 19:26:01 +04:30
Hamidreza Ghavami
fc0882805d update UI to use password-input component 2023-05-08 19:24:44 +04:30
Hamidreza Ghavami
f553922d53 add password component 2023-05-08 19:21:58 +04:30
Hamidreza Ghavami
7b2764566c update login UI 2023-05-08 19:20:13 +04:30
Hamidreza Ghavami
55d38dfa48 [FIX] bug logout path 2023-05-08 19:15:33 +04:30
Hamidreza Ghavami
0e266b88f0 update UI to use themeSwitcher 2023-05-08 19:14:22 +04:30
MHSanaei
7bb3e517b2 update pic v1.4.1 2023-05-08 17:39:29 +03:30
Hamidreza Ghavami
7d0c3b6517 remove themeChanger from siderDrawer 2023-05-08 18:19:59 +04:30
Hamidreza Ghavami
67201fc678 create theme-switch component 2023-05-08 18:15:08 +04:30
Hamidreza Ghavami
d137deccfa fix style height when rotating + move cookie util to their specific file 2023-05-08 18:04:12 +04:30
MHSanaei
00777e3a25 [feature] Russian lang 2023-05-08 14:43:02 +03:30
koid38
bcb2f125ff Create translate.ru_RU.toml (#375)
ru translate
2023-05-08 14:05:16 +03:30
MHSanaei
37ab8f42e9 domain-list-community (category update)
add cn and ru regexp
2023-05-08 13:50:43 +03:30
Ho3ein
cf1cfbee96 v1.4.1 2023-05-08 10:55:28 +03:30
MHSanaei
d89e03023f + 2023-05-08 10:40:32 +03:30
Tara Rostami
5ec1559c7b UI optimized by Tara (#370)
* Update antd.min.css

* Update custom.css

---------

Co-authored-by: Ho3ein <ho3ein.sanaei@gmail.com>
2023-05-08 10:32:51 +03:30
Ho3ein
cf2b1fd9ec Merge pull request #364 from M4hbod/main
Update UI
2023-05-08 10:20:47 +03:30
itspooya
ddbc1602ce Finally - Fix WorkDIR 2023-05-08 10:04:47 +03:30
itspooya
77cee098ad Finally 2023-05-08 10:04:47 +03:30
itspooya
89b94c2c90 Enable CGO 2023-05-08 10:04:47 +03:30
itspooya
350743fea3 Enable CGO 2023-05-08 10:04:47 +03:30
itspooya
8011d0b6c6 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
9e5d7ac1d0 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
e01fb9b605 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
52a468d586 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
c73c71cc83 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
2141d62069 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
f286c9a86a Added ARM64 2023-05-08 10:04:47 +03:30
itspooya
b4fd254c71 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
0a8cf7e41b 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
abd48551fd 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
bb9a10051f 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
5c6406ab58 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
a25137f215 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
50c296eb28 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
b1a302de95 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
7b567458ff 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
13e3e23f20 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
efecdf5fd0 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
f21904caf2 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
15a97af215 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
20a55c086e 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
c727b81772 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
77692e0298 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
Mahbod
5e3e965647 add margin to .ant-modal 2023-05-08 03:36:52 +03:30
Mahbod
88cde18bb2 update .ant-modal ui 2023-05-08 02:55:06 +03:30
77 changed files with 3220 additions and 1924 deletions

View File

@@ -11,7 +11,7 @@ jobs:
steps: steps:
- name: Check out the code - name: Check out the code
uses: actions/checkout@v3.5.2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0 uses: docker/setup-qemu-action@v2.1.0
@@ -36,6 +36,6 @@ jobs:
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64, linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3.5.2 - uses: actions/checkout@v3.5.2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4.0.0 uses: actions/setup-go@v4.0.1
with: with:
go-version: "stable" go-version: "stable"
- name: build linux amd64 version - name: build linux amd64 version
@@ -52,7 +52,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3.5.2 - uses: actions/checkout@v3.5.2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4.0.0 uses: actions/setup-go@v4.0.1
with: with:
go-version: "stable" go-version: "stable"
- name: build linux arm64 version - name: build linux arm64 version

22
DockerInit.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
if [ $1 == "amd64" ]; then
ARCH="64";
FNAME="amd64";
elif [ $1 == "arm64" ]; then
ARCH="arm64-v8a"
FNAME="arm64";
else
ARCH="64";
FNAME="amd64";
fi
mkdir -p build/bin
cd build/bin
wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../

View File

@@ -1,54 +1,20 @@
# Use the official Golang image as the base image #Build latest x-ui from source
FROM golang:1.20 as builder FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
ARG TARGETARCH
ARG TARGETOS
# Set up the working directory
WORKDIR /app WORKDIR /app
ARG TARGETARCH
# Copy the Go modules and download the dependencies RUN apk --no-cache --update add build-base gcc wget unzip
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . . COPY . .
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
# Build the X-ui binary
RUN CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o xui-release-${TARGETARCH} -v main.go
# Start a new stage using the base image #Build app image using latest x-ui
FROM ubuntu:20.04 FROM alpine
ENV TZ=Asia/Tehran
ARG TARGETARCH
# Set up the working directory
WORKDIR /app WORKDIR /app
# Copy the X-ui binary and required files from the builder stage RUN apk add ca-certificates tzdata
COPY --from=builder /app/xui-release-${TARGETARCH} /app/x-ui/xui-release
COPY x-ui.service /app/x-ui/x-ui.service
COPY x-ui.sh /app/x-ui/x-ui.sh
# Set up the runtime environment COPY --from=builder /app/build/ /app/
RUN apt-get update && apt-get install -y \ VOLUME [ "/etc/x-ui" ]
wget \ ENTRYPOINT [ "/app/x-ui" ]
unzip \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app/x-ui/bin
# Download and set up the required files
RUN wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip \
&& unzip Xray-linux-64.zip \
&& rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat \
&& wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat \
&& wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat \
&& wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat \
&& mv xray xray-linux-\${TARGETARCH}
WORKDIR /app
RUN chmod +x /app/x-ui/x-ui.sh
# Set the entrypoint
ENTRYPOINT ["/app/x-ui/x-ui.sh"]

View File

@@ -1,4 +1,5 @@
# 3x-ui # 3x-ui
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment** > **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
@@ -7,12 +8,13 @@
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)** 3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
**If you think this project is helpful to you, you may wish to give a** :star2: **If you think this project is helpful to you, you may wish to give a** :star2:
**Buy Me a Coffee :** **Buy Me a Coffee :**
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
# Install & Upgrade # Install & Upgrade
@@ -22,10 +24,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## Install custom version ## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.4.0`: To install your desired version you can add the version to the end of install command. Example for ver `v1.5.0`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.4.0 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.5.0
``` ```
# SSL # SSL
@@ -47,12 +49,12 @@ or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
Before you set ssl on settings Before you set ssl on settings
- http://ip:2053/xui - http://ip:2053/panel
- http://domain:2053/xui - http://domain:2053/panel
After you set ssl on settings After you set ssl on settings
- https://yourdomain:2053/xui - https://yourdomain:2053/panel
# Environment Variables # Environment Variables
@@ -69,6 +71,31 @@ Example:
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
``` ```
# Install with Docker
1. Install Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Run 3x-ui:
```sh
docker compose up -d
```
OR
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
# Xray Configurations: # Xray Configurations:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install) **copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
@@ -160,7 +187,7 @@ Reference syntax:
## API routes ## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login - `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions: - `/panel/api/inbounds` base for following actions:
| Method | Path | Action | | Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- | | :----: | ---------------------------------- | ------------------------------------------- |
@@ -173,19 +200,19 @@ Reference syntax:
| `POST` | `"/clientIps/:email"` | Client Ip address | | `POST` | `"/clientIps/:email"` | Client Ip address |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address | | `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
| `POST` | `"/addClient"` | Add Client to inbound | | `POST` | `"/addClient"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId* | | `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId* | | `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic | | `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds | | `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound | | `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) | | `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
*- The field `clientId` should be filled by: \*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS - `client.id` for VMESS and VLESS
- `client.password` for TROJAN - `client.password` for TROJAN
- `client.email` for Shadowsocks - `client.email` for Shadowsocks
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044) - [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
# A Special Thanks To # A Special Thanks To
@@ -205,6 +232,8 @@ Reference syntax:
![2](./media/2.png) ![2](./media/2.png)
![3](./media/3.png) ![3](./media/3.png)
![4](./media/4.png) ![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
## Stargazers over time ## Stargazers over time

View File

@@ -1 +1 @@
1.4.0 1.5.0

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
---
version: "3.9"
services:
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: yourhostname
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
network_mode: host
restart: unless-stopped

10
go.mod
View File

@@ -7,8 +7,8 @@ require (
github.com/gin-contrib/sessions v0.0.4 github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.0
github.com/go-cmd/cmd v1.4.1 github.com/go-cmd/cmd v1.4.1
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/mymmrac/telego v0.24.0
github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7 github.com/pelletier/go-toml/v2 v2.0.7
@@ -19,13 +19,15 @@ require (
golang.org/x/text v0.9.0 golang.org/x/text v0.9.0
google.golang.org/grpc v1.55.0 google.golang.org/grpc v1.55.0
gorm.io/driver/sqlite v1.5.0 gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0 gorm.io/gorm v1.25.1
) )
require ( require (
github.com/BurntSushi/toml v1.2.1 // indirect github.com/BurntSushi/toml v1.2.1 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bytedance/sonic v1.8.8 // indirect github.com/bytedance/sonic v1.8.8 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fasthttp/router v1.4.19 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
@@ -38,6 +40,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
@@ -47,11 +50,14 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.47.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.8.0 // indirect golang.org/x/crypto v0.8.0 // indirect

18
go.sum
View File

@@ -4,6 +4,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw= github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
@@ -18,6 +19,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
@@ -45,8 +48,6 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -81,6 +82,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
@@ -105,6 +107,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.24.0 h1:0fd+v2/dToL6/DtsnWr+2saK7ZxIgLY+LI9kqJQbPEo=
github.com/mymmrac/telego v0.24.0/go.mod h1:y557P/iMHSaOVDi5Nmy1gNelqrw+jaBMvP9guPaNJsQ=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
@@ -133,6 +137,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk= github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw= github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o= github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8= github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
@@ -164,6 +170,10 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE= github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4= github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM= github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
@@ -252,8 +262,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -25,9 +25,9 @@ echo "The OS release is: $release"
arch3xui() { arch3xui() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64 | x64 | amd64 ) echo 'amd64' ;; x86_64 | x64 | amd64) echo 'amd64' ;;
armv8 | arm64 | aarch64 ) echo 'arm64' ;; armv8 | arm64 | aarch64) echo 'arm64' ;;
* ) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac esac
} }
echo "arch: $(arch3xui)" echo "arch: $(arch3xui)"
@@ -39,7 +39,7 @@ if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
fi fi
@@ -59,18 +59,17 @@ fi
install_base() { install_base() {
case "${release}" in case "${release}" in
centos|fedora) centos | fedora)
yum install -y -q wget curl tar yum install -y -q wget curl tar
;; ;;
*) *)
apt install -y -q wget curl tar apt install -y -q wget curl tar
;; ;;
esac esac
} }
#This function will be called when user installed x-ui out of sercurity #This function will be called when user installed x-ui out of sercurity
config_after_install() { config_after_install() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
@@ -101,6 +100,7 @@ config_after_install() {
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}" echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
fi fi
fi fi
/usr/local/x-ui/x-ui migrate
} }
install_x-ui() { install_x-ui() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -6,8 +6,6 @@ import (
"x-ui/logger" "x-ui/logger"
) )
var CtxDone = errors.New("context done")
func NewErrorf(format string, a ...interface{}) error { func NewErrorf(format string, a ...interface{}) error {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
return errors.New(msg) return errors.New(msg)

View File

@@ -1,9 +0,0 @@
package common
import "sort"
func IsSubString(target string, str_array []string) bool {
sort.Strings(str_array)
index := sort.SearchStrings(str_array, target)
return index < len(str_array) && str_array[index] == target
}

View File

@@ -1,12 +0,0 @@
package util
import "context"
func IsDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}

View File

@@ -6,7 +6,7 @@ import (
type RawMessage []byte type RawMessage []byte
// MarshalJSON 自定义 json.RawMessage 默认行为 // MarshalJSON: Customize json.RawMessage default behavior
func (m RawMessage) MarshalJSON() ([]byte, error) { func (m RawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 { if len(m) == 0 {
return []byte("null"), nil return []byte("null"), nil
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
return m, nil return m, nil
} }
// UnmarshalJSON sets *m to a copy of data. // UnmarshalJSON: sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error { func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil { if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")

View File

View File

@@ -31,9 +31,9 @@ small{font-size:80%}
sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline} sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}
sub{bottom:-.25em} sub{bottom:-.25em}
sup{top:-.5em} sup{top:-.5em}
a{color:#1890ff;text-decoration:none;background-color:transparent;outline:0;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects} a{color:rgb(0 150 112);text-decoration:none;background-color:transparent;outline:0;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects}
a:hover{color:#40a9ff} a:hover{color:rgb(0 150 112)}
a:active{color:#096dd9} a:active{color:rgb(10, 105, 82)}
a:active,a:hover{text-decoration:none;outline:0} a:active,a:hover{text-decoration:none;outline:0}
a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none} a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none}
code,kbd,pre,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace} code,kbd,pre,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}
@@ -520,8 +520,8 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-arrow .ant-select-arrow-icon{display:block} .ant-select-arrow .ant-select-arrow-icon{display:block}
.ant-select-arrow .ant-select-arrow-icon svg{transition:transform .3s} .ant-select-arrow .ant-select-arrow-icon svg{transition:transform .3s}
.ant-select-selection{display:block;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:4px;outline:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-select-selection{display:block;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:4px;outline:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-select-selection:hover{border-color:#40a9ff;border-right-width:1px!important} .ant-select-selection:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:rgb(0, 150, 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease;text-rendering:auto} .ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease;text-rendering:auto}
.ant-select-selection__clear:before{display:block} .ant-select-selection__clear:before{display:block}
.ant-select-selection__clear:hover{color:rgba(0,0,0,.45)} .ant-select-selection__clear:hover{color:rgba(0,0,0,.45)}
@@ -529,7 +529,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis} .ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0} .ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}
.ant-select-disabled{color:rgba(0,0,0,.25)} .ant-select-disabled{color:rgba(0,0,0,.25)}
.ant-select-disabled .ant-select-selection{background:#f5f5f5;cursor:not-allowed} .ant-select-disabled .ant-select-selection{background:rgb(221, 221, 221);cursor:not-allowed}
.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none} .ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none}
.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none} .ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}
.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5} .ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}
@@ -582,7 +582,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px} .ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px}
.ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px} .ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px}
.ant-select-open .ant-select-arrow-icon svg{transform:rotate(180deg)} .ant-select-open .ant-select-arrow-icon svg{transform:rotate(180deg)}
.ant-select-open .ant-select-selection{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-select-open .ant-select-selection{border-color:rgb(0 150 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-select-combobox .ant-select-arrow{display:none} .ant-select-combobox .ant-select-arrow{display:none}
.ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%} .ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%}
.ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%} .ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%}
@@ -629,8 +629,8 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-input:-moz-placeholder-shown{text-overflow:ellipsis} .ant-input:-moz-placeholder-shown{text-overflow:ellipsis}
.ant-input:-ms-input-placeholder{text-overflow:ellipsis} .ant-input:-ms-input-placeholder{text-overflow:ellipsis}
.ant-input:placeholder-shown{text-overflow:ellipsis} .ant-input:placeholder-shown{text-overflow:ellipsis}
.ant-input:focus,.ant-input:hover{border-color:#40a9ff;border-right-width:1px!important} .ant-input:focus,.ant-input:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important;}
.ant-input:focus{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-input:focus{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important} .ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
.ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
@@ -716,7 +716,7 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px} .ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}
.ant-btn>a:only-child{color:currentColor} .ant-btn>a:only-child{color:currentColor}
.ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn:focus,.ant-btn:hover{color:#40a9ff;background-color:#fff;border-color:#40a9ff} .ant-btn:focus,.ant-btn:hover{color:rgb(0 150 112);background-color:#fff;border-color:rgb(0 150 112)}
.ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor} .ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor}
.ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn.active,.ant-btn:active{color:#096dd9;background-color:#fff;border-color:#096dd9} .ant-btn.active,.ant-btn:active{color:#096dd9;background-color:#fff;border-color:#096dd9}
@@ -727,16 +727,16 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff} .ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff}
.ant-btn>i,.ant-btn>span{display:inline-block;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none} .ant-btn>i,.ant-btn>span{display:inline-block;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none}
.ant-btn-primary{color:#fff;background-color:#1890ff;border-color:#1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)} .ant-btn-primary{color:#fff;background-color:rgb(0, 150, 112);border-color:rgb(0, 150, 112);text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-primary>a:only-child{color:currentColor} .ant-btn-primary>a:only-child{color:currentColor}
.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:#40a9ff;border-color:#00000017} .ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:rgb(0, 185, 138);border-color:#00000017}
.ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor} .ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor}
.ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:#096dd9;border-color:#096dd9} .ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:rgb(25, 191, 149);border-color:rgb(25, 191, 149)}
.ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor} .ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor}
.ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor} .ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor}
.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#40a9ff;border-left-color:#40a9ff} .ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#40a9ff;border-left-color:#40a9ff}
@@ -769,16 +769,16 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}
.ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor} .ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor}
.ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)} .ant-btn-danger{color:#ff4d4f;background-color:rgb(255 77 79 / 0%);border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-danger>a:only-child{color:currentColor} .ant-btn-danger>a:only-child{color:currentColor}
.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff7875;border-color:#ff7875} .ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f}
.ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor} .ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor}
.ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e} .ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e}
.ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor} .ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor}
.ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor} .ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor}
.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;box-shadow:none} .ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;box-shadow:none}
@@ -992,11 +992,11 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)} .ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)}
.ant-menu-item>.ant-badge>a:hover{color:#1890ff} .ant-menu-item>.ant-badge>a:hover{color:#1890ff}
.ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8} .ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8}
.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:#1890ff;background-color:#ebebeb} .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:#2d2d2d;background-image: linear-gradient(90deg,#99999980 0,#8888889e 100%);border-radius: 0.5rem}
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px} .ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent} .ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff} .ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color:#e6f7ff} .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
.ant-menu-vertical-right{border-left:1px solid #e8e8e8} .ant-menu-vertical-right{border-left:1px solid #e8e8e8}
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0} .ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0} .ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
@@ -1032,7 +1032,7 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-horizontal>.ant-menu-item-selected>a{color:#1890ff} .ant-menu-horizontal>.ant-menu-item-selected>a{color:#1890ff}
.ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"} .ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"}
.ant-menu-inline .ant-menu-item,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item{position:relative} .ant-menu-inline .ant-menu-item,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item{position:relative}
.ant-menu-inline .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0;border-right:3px solid #1890ff;transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""} .ant-menu-inline .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0/*;border-right:3px solid #1890ff*/;transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""}
.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis} .ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis}
.ant-menu-inline .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu{padding-bottom:.02px} .ant-menu-inline .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu{padding-bottom:.02px}
.ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child){margin-bottom:8px} .ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child){margin-bottom:8px}
@@ -1080,9 +1080,9 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-dark .ant-menu-item:hover{background-color:transparent} .ant-menu-dark .ant-menu-item:hover{background-color:transparent}
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0} .ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
.ant-menu-dark .ant-menu-item-selected:after{border-right:0} .ant-menu-dark .ant-menu-item-selected:after{border-right:0}
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#206bd3} .ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a} .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#15223a}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#383838} .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8} .ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important} .ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important} .ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
@@ -1236,18 +1236,18 @@ span.ant-radio+*{padding-right:8px;padding-left:8px}
.ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:4px 0 0 4px} .ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:4px 0 0 4px}
.ant-radio-button-wrapper:last-child{border-radius:0 4px 4px 0} .ant-radio-button-wrapper:last-child{border-radius:0 4px 4px 0}
.ant-radio-button-wrapper:first-child:last-child{border-radius:4px} .ant-radio-button-wrapper:first-child:last-child{border-radius:4px}
.ant-radio-button-wrapper:hover{position:relative;color:#1890ff} .ant-radio-button-wrapper:hover{position:relative;color:#009670}
.ant-radio-button-wrapper:focus-within{outline:3px solid rgba(24,144,255,.06)} .ant-radio-button-wrapper:focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none} .ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#1890ff;background:#fff;border-color:#1890ff;box-shadow:-1px 0 0 0 #1890ff} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#009670;background:#fff;border-color:#009670;box-shadow:-1px 0 0 0 #009670}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#1890ff!important;opacity:.1} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#009670!important;opacity:.1}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#1890ff;box-shadow:none!important} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#009670;box-shadow:none!important}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#40a9ff;border-color:#40a9ff;box-shadow:-1px 0 0 0 #40a9ff} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#009670;border-color:#009670;box-shadow:-1px 0 0 0 #009670}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#096dd9;border-color:#096dd9;box-shadow:-1px 0 0 0 #096dd9} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#076e54;border-color:#076e54;box-shadow:-1px 0 0 0 #076e54}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)} .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#1890ff;border-color:#1890ff} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#009670;border-color:#009670}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#40a9ff;border-color:#40a9ff} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#009670;border-color:#009670}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#096dd9;border-color:#096dd9} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#076e54;border-color:#076e54}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)} .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-button-wrapper-disabled{cursor:not-allowed} .ant-radio-button-wrapper-disabled{cursor:not-allowed}
.ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9} .ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9}
@@ -1267,8 +1267,8 @@ to{transform:scale(1.6);opacity:0}
.ant-card{box-sizing:border-box;margin:auto 10px 3px auto;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-card{box-sizing:border-box;margin:auto 10px 3px auto;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-card-hoverable{cursor:pointer} .ant-card-hoverable{cursor:pointer}
.ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)} .ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)}
.ant-card-bordered{/*! border:1px solid #e8e8e8; *//*! box-shadow: 0px 6px 0px 0px rgb(15, 197, 254); *//*! box-shadow: 0 4px 5px 0 rgba(170, 179, 217, 0.33); */backdrop-filter:blur(7px) saturate(200%);background-color:#fff;border:0 solid rgba(255,255,255,0);box-shadow:0 1px 7px -1px #0000005c} .ant-card-bordered{/*! border:1px solid #e8e8e8; *//*! box-shadow: 0px 6px 0px 0px rgb(15, 197, 254); *//*! box-shadow: 0 4px 5px 0 rgba(170, 179, 217, 0.33); */backdrop-filter:blur(7px) saturate(200%);background-color:#fff;border:0 solid rgba(255,255,255,0)/*;box-shadow:0 1px 7px -1px #0000005c*/}
.ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:0 0;border-bottom:1px solid #e8e8e8;border-radius:2px 2px 0 0;zoom:1} .ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:0 0;border-bottom:2px solid rgba(155, 155, 155, 0.15);border-radius:2px 2px 0 0;zoom:1}
.ant-card-head:after,.ant-card-head:before{display:table;content:""} .ant-card-head:after,.ant-card-head:before{display:table;content:""}
.ant-card-head:after{clear:both} .ant-card-head:after{clear:both}
.ant-card-head-wrapper{display:flex;align-items:center} .ant-card-head-wrapper{display:flex;align-items:center}
@@ -1357,11 +1357,11 @@ to{transform:scale(1.6);opacity:0}
.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px} .ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px}
.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px} .ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px}
.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#1890ff} .ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#1890ff}
.ant-tabs{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;overflow:hidden;zoom:1;border-radius:10px;box-shadow:0 1px 7px -1px #0000005c;transition:all .3s} .ant-tabs{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;overflow:hidden;zoom:1;border-radius:1.5rem;/*box-shadow:0 1px 7px -1px #0000005c;*/transition:all .3s;background-color: white}
.ant-tabs:hover{box-shadow:0 3px 12px -.8px #0000005c} .ant-tabs:hover{box-shadow:0 3px 12px -.8px #0000005c}
.ant-tabs:after,.ant-tabs:before{display:table;content:""} .ant-tabs:after,.ant-tabs:before{display:table;content:""}
.ant-tabs:after{clear:both} .ant-tabs:after{clear:both}
.ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:#1890ff;transform-origin:0 0} .ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:rgb(0 150 112);transform-origin:0 0}
.ant-tabs-bar{margin:1.5rem 1.5rem 0rem 1.5rem!important;border-bottom:2px solid rgb(153 153 153 / 20%);outline:0} .ant-tabs-bar{margin:1.5rem 1.5rem 0rem 1.5rem!important;border-bottom:2px solid rgb(153 153 153 / 20%);outline:0}
.ant-tabs-bar,.ant-tabs-nav-container{transition:padding .3s cubic-bezier(.645,.045,.355,1)} .ant-tabs-bar,.ant-tabs-nav-container{transition:padding .3s cubic-bezier(.645,.045,.355,1)}
.ant-tabs-nav-container{position:relative;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1} .ant-tabs-nav-container{position:relative;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1}
@@ -1390,10 +1390,10 @@ to{transform:scale(1.6);opacity:0}
.ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1)} .ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1)}
.ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:4px 4px 0 0;transition:all .3s;content:"";pointer-events:none} .ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:4px 4px 0 0;transition:all .3s;content:"";pointer-events:none}
.ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0} .ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0}
.ant-tabs-nav .ant-tabs-tab:hover{color:#40a9ff} .ant-tabs-nav .ant-tabs-tab:hover{color:rgb(0 150 112)}
.ant-tabs-nav .ant-tabs-tab:active{color:#096dd9} .ant-tabs-nav .ant-tabs-tab:active{color:rgb(0 150 112)}
.ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px} .ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px}
.ant-tabs-nav .ant-tabs-tab-active{color:#1890ff;font-weight:500} .ant-tabs-nav .ant-tabs-tab-active{color:rgb(0 150 112);font-weight:500}
.ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed} .ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}
.ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px} .ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px}
.ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px} .ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px}
@@ -2438,14 +2438,14 @@ to{transform:scale(1.6);opacity:0}
.ant-cascader-menu-item-disabled.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-disabled.ant-cascader-menu-item-loading-icon{color:rgba(0,0,0,.25)} .ant-cascader-menu-item-disabled.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-disabled.ant-cascader-menu-item-loading-icon{color:rgba(0,0,0,.25)}
.ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d} .ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d}
.ant-checkbox{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;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:0;cursor:pointer} .ant-checkbox{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;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:0;cursor:pointer}
.ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:#1890ff} .ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:rgb(0, 150, 112)}
.ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""} .ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid rgb(0, 150, 112);border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}
.ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible} .ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible}
.ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s} .ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s}
.ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "} .ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}
.ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0} .ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}
.ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "} .ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}
.ant-checkbox-checked .ant-checkbox-inner{background-color:#1890ff;border-color:#1890ff} .ant-checkbox-checked .ant-checkbox-inner{background-color:rgb(0, 150, 112);border-color:rgb(0, 150, 112)}
.ant-checkbox-disabled{cursor:not-allowed} .ant-checkbox-disabled{cursor:not-allowed}
.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none} .ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none}
.ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed} .ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed}
@@ -2464,9 +2464,9 @@ to{transform:scale(1.6);opacity:0}
.ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9} .ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9}
.ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "} .ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}
.ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)} .ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}
.ant-collapse{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";background-color:#fafafa;border:1px solid #d9d9d9;border-bottom:0;border-radius:4px} .ant-collapse{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"/*;background-color:#fafafa*/;border:1px solid rgba(100, 100, 100, 0.2);/*border-bottom:0;*/border-radius:0.5rem}
.ant-collapse>.ant-collapse-item{border-bottom:1px solid #d9d9d9} .ant-collapse>.ant-collapse-item{border-bottom:1px solid rgb(217 217 217 / 20%)}
.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0 0 4px 4px} .ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0.5rem}
.ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;transition:all .3s} .ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;transition:all .3s}
.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;transform:translateY(-50%)} .ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;transform:translateY(-50%)}
.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1} .ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1}
@@ -2480,7 +2480,7 @@ to{transform:scale(1.6);opacity:0}
.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px} .ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px}
.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto} .ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto}
.ant-collapse-anim-active{transition:height .2s cubic-bezier(.215,.61,.355,1)} .ant-collapse-anim-active{transition:height .2s cubic-bezier(.215,.61,.355,1)}
.ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid #d9d9d9} .ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid rgb(0 150 112)}
.ant-collapse-content>.ant-collapse-content-box{padding:16px} .ant-collapse-content>.ant-collapse-content-box{padding:16px}
.ant-collapse-content-inactive{display:none} .ant-collapse-content-inactive{display:none}
.ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 4px 4px} .ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 4px 4px}
@@ -2539,7 +2539,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-picker-input{outline:0} .ant-calendar-picker-input{outline:0}
.ant-calendar-picker-input.ant-input{line-height:1.5} .ant-calendar-picker-input.ant-input{line-height:1.5}
.ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0} .ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0}
.ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff} .ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:rgb(0 150 112)}
.ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none} .ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none}
@@ -2549,7 +2549,7 @@ to{transform:scale(1.6);opacity:0}
.ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed} .ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed}
.ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px} .ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px}
.ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:4px;outline:0;box-shadow:0 2px 8px rgba(0,0,0,.15)} .ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:4px;outline:0;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid #e8e8e8} .ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid rgb(153 153 153 / 25%)}
.ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto} .ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto}
.ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1} .ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1}
.ant-calendar-input:-ms-input-placeholder{color:#bfbfbf} .ant-calendar-input:-ms-input-placeholder{color:#bfbfbf}
@@ -2559,7 +2559,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-input:placeholder-shown{text-overflow:ellipsis} .ant-calendar-input:placeholder-shown{text-overflow:ellipsis}
.ant-calendar-week-number{width:286px} .ant-calendar-week-number{width:286px}
.ant-calendar-week-number-cell{text-align:center} .ant-calendar-week-number-cell{text-align:center}
.ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid rgb(153 153 153 / 25%);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-header a:hover{color:#40a9ff} .ant-calendar-header a:hover{color:#40a9ff}
.ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px} .ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}
.ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none} .ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none}
@@ -2595,8 +2595,8 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:0 0;border:1px solid transparent;border-radius:2px;transition:background .3s ease} .ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:0 0;border:1px solid transparent;border-radius:2px;transition:background .3s ease}
.ant-calendar-date-panel{position:relative;outline:0} .ant-calendar-date-panel{position:relative;outline:0}
.ant-calendar-date:hover{background:#e6f7ff;cursor:pointer} .ant-calendar-date:hover{background:#e6f7ff;cursor:pointer}
.ant-calendar-date:active{color:#fff;background:#40a9ff} .ant-calendar-date:active{color:rgba(0,0,0,.65);background:#bae7ff}
.ant-calendar-today .ant-calendar-date{color:#1890ff;font-weight:700;border-color:#1890ff} .ant-calendar-today .ant-calendar-date{color:rgb(0 150 112);font-weight:700;border-color:rgb(0 150 112)}
.ant-calendar-selected-day .ant-calendar-date{background:#bae7ff} .ant-calendar-selected-day .ant-calendar-date{background:#bae7ff}
.ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:0 0;border-color:transparent} .ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:0 0;border-color:transparent}
.ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed} .ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed}
@@ -2606,7 +2606,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "} .ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "}
.ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px} .ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px}
.ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px} .ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px}
.ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid #e8e8e8} .ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid rgb(153 153 153 / 25%)}
.ant-calendar-footer:empty{border-top:0} .ant-calendar-footer:empty{border-top:0}
.ant-calendar-footer-btn{display:block;text-align:center} .ant-calendar-footer-btn{display:block;text-align:center}
.ant-calendar-footer-extra{text-align:left} .ant-calendar-footer-extra{text-align:left}
@@ -2616,7 +2616,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px} .ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px}
.ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;transition:color .3s ease} .ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;transition:color .3s ease}
.ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)} .ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)}
.ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:#1890ff;border:1px solid #1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px} .ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:rgb(0 150 112);border:1px solid rgb(0 150 112);text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px}
.ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1} .ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1}
.ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0} .ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0}
.ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none} .ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none}
@@ -2627,13 +2627,13 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px} .ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}
.ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:#40a9ff;border-color:#40a9ff} .ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:rgb(0 150 112);border-color:rgb(0 150 112)}
.ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#096dd9;border-color:#096dd9} .ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#096dd9;border-color:#096dd9}
.ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none} .ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor} .ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""} .ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0} .ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0}
@@ -2944,7 +2944,7 @@ textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-h
.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2} .ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}
.ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f} .ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f}
.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911} .ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}
.ant-tag-green{color:#52c41a;background:#f6ffed;border-color:#b7eb8f} .ant-tag-green{color:#19bf95;background:#ebfffa;border-color:#8ce7d0}
.ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a} .ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a}
.ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff} .ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff}
.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff} .ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}
@@ -2980,7 +2980,7 @@ textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-h
.ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle} .ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle}
.ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0} .ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0}
.ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:0 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:0 0} .ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:0 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:0 0}
.ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid #e8e8e8;transform:translateY(50%);content:""} .ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid rgb(0 150 112 / 50%);transform:translateY(50%);content:""}
.ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px} .ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px}
.ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%} .ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%}
.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%} .ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%}
@@ -3216,7 +3216,7 @@ to{transform:scale(1)}
.ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis} .ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis}
.ant-input-number:-ms-input-placeholder{text-overflow:ellipsis} .ant-input-number:-ms-input-placeholder{text-overflow:ellipsis}
.ant-input-number:placeholder-shown{text-overflow:ellipsis} .ant-input-number:placeholder-shown{text-overflow:ellipsis}
.ant-input-number:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-input-number:focus{border-color:rgb(0, 150, 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px !important}
.ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important} .ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}
textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s} textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}
@@ -3230,7 +3230,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block} .ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block}
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none} .ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block} .ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
.ant-input-number-focused,.ant-input-number:hover{border-color:#40a9ff;border-right-width:1px!important} .ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)} .ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1} .ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important} .ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
@@ -3261,7 +3261,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-input-number-handler-down:hover{height:60%!important} .ant-input-number-handler-down:hover{height:60%!important}
.ant-input-number-handler-down-disabled,.ant-input-number-handler-up-disabled{cursor:not-allowed} .ant-input-number-handler-down-disabled,.ant-input-number-handler-up-disabled{cursor:not-allowed}
.ant-input-number-handler-down-disabled:hover .ant-input-number-handler-down-inner,.ant-input-number-handler-up-disabled:hover .ant-input-number-handler-up-inner{color:rgba(0,0,0,.25)} .ant-input-number-handler-down-disabled:hover .ant-input-number-handler-down-inner,.ant-input-number-handler-up-disabled:hover .ant-input-number-handler-up-inner{color:rgba(0,0,0,.25)}
.ant-layout{display:flex;flex:auto;flex-direction:column;min-height:0;background:#fff} .ant-layout{display:flex;flex:auto;flex-direction:column;min-height:0;background:#f0f2f5}
.ant-layout,.ant-layout *{box-sizing:border-box} .ant-layout,.ant-layout *{box-sizing:border-box}
.ant-layout.ant-layout-has-sider{flex-direction:row} .ant-layout.ant-layout-has-sider{flex-direction:row}
.ant-layout.ant-layout-has-sider>.ant-layout,.ant-layout.ant-layout-has-sider>.ant-layout-content{overflow-x:hidden} .ant-layout.ant-layout-has-sider>.ant-layout,.ant-layout.ant-layout-has-sider>.ant-layout-content{overflow-x:hidden}
@@ -3270,7 +3270,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5} .ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5}
.ant-layout-content{flex:auto;min-height:0} .ant-layout-content{flex:auto;min-height:0}
.ant-layout-sider{position:relative;min-width:0;background:#001529;transition:all .2s} .ant-layout-sider{position:relative;min-width:0;background:#001529;transition:all .2s}
.ant-layout-sider-children{height:100%;margin-top:-.1px;padding-top:.1px} .ant-layout-sider-children{height:100%;margin-top:-.1px;padding:0.5rem}
.ant-layout-sider-has-trigger{padding-bottom:48px} .ant-layout-sider-has-trigger{padding-bottom:48px}
.ant-layout-sider-right{order:1} .ant-layout-sider-right{order:1}
.ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;transition:all .2s} .ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;transition:all .2s}
@@ -3278,7 +3278,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-layout-sider-zero-width-trigger{position:absolute;top:64px;right:-36px;z-index:1;width:36px;height:42px;color:#fff;font-size:18px;line-height:42px;text-align:center;background:#001529;border-radius:0 4px 4px 0;cursor:pointer;transition:background .3s ease} .ant-layout-sider-zero-width-trigger{position:absolute;top:64px;right:-36px;z-index:1;width:36px;height:42px;color:#fff;font-size:18px;line-height:42px;text-align:center;background:#001529;border-radius:0 4px 4px 0;cursor:pointer;transition:background .3s ease}
.ant-layout-sider-zero-width-trigger:hover{background:#192c3e} .ant-layout-sider-zero-width-trigger:hover{background:#192c3e}
.ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:4px 0 0 4px} .ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:4px 0 0 4px}
.ant-layout-sider-light{background:#f7f7f7} .ant-layout-sider-light{background:#fff}
.ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff} .ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff}
.ant-list{box-sizing:border-box;margin:1.5em;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} .ant-list{box-sizing:border-box;margin:1.5em;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}
.ant-list *{outline:0} .ant-list *{outline:0}
@@ -3532,9 +3532,9 @@ to{max-height:0;padding:0;opacity:0}
.ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:0 0;border:0;outline:0;cursor:pointer;transition:color .3s} .ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:0 0;border:0;outline:0;cursor:pointer;transition:color .3s}
.ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto} .ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto}
.ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none} .ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none}
.ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:4px 4px 0 0} .ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid rgba(100, 100, 100, 0.2);border-radius:4px 4px 0 0}
.ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word} .ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}
.ant-modal-footer{padding:10px 16px;text-align:right;background:0 0;border-top:1px solid #e8e8e8;border-radius:0 0 4px 4px} .ant-modal-footer{padding:10px 16px;text-align:right;background:0 0;border-top:1px solid rgba(100, 100, 100, 0.2);border-radius:0 0 4px 4px}
.ant-modal-footer button+button{margin-bottom:0;margin-left:8px} .ant-modal-footer button+button{margin-bottom:0;margin-left:8px}
.ant-modal.zoom-appear,.ant-modal.zoom-enter{transform:none;opacity:0;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-modal.zoom-appear,.ant-modal.zoom-enter{transform:none;opacity:0;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)} .ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)}
@@ -3673,14 +3673,15 @@ to{max-height:0;margin-bottom:0;padding-top:0;padding-bottom:0;opacity:0}
.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px} .ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px}
.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px} .ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}
.ant-progress{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";display:inline-block} .ant-progress{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";display:inline-block}
.ant-progress{box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;}
.ant-progress-line{position:relative;width:100%;font-size:14px} .ant-progress-line{position:relative;width:100%;font-size:14px}
.ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px} .ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px}
.ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0} .ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0}
.ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)} .ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)}
.ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#f5f5f5;border-radius:100px} .ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#f5f5f5;border-radius:100px}
.ant-progress-circle-trail{stroke:#090b0e29} .ant-progress-circle-trail{stroke:rgb(100 100 100 / 15%) !important}
.ant-progress-circle-path{-webkit-animation:ant-progress-appear .3s;animation:ant-progress-appear .3s} .ant-progress-circle-path{-webkit-animation:ant-progress-appear .3s;animation:ant-progress-appear .3s}
.ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#1890ff} .ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:rgb(0 150 112) !important}
.ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s} .ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s}
.ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#52c41a} .ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#52c41a}
.ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal} .ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal}
@@ -3968,7 +3969,7 @@ to{background-position:0 50%}
.ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px} .ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px}
.ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px} .ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px}
.ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;transform:scale(.66667)} .ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;transform:scale(.66667)}
.ant-switch-checked{background-color:#1890ff} .ant-switch-checked{background-color:rgb(0 150 112)}
.ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px} .ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px}
.ant-switch-checked:after{left:100%;margin-left:-1px;transform:translateX(-100%)} .ant-switch-checked:after{left:100%;margin-left:-1px;transform:translateX(-100%)}
.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px} .ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px}
@@ -3990,7 +3991,7 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important} .ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important}
.ant-table table{width:100%;text-align:left;border-radius:4px 4px 0 0;border-collapse:separate;border-spacing:0} .ant-table table{width:100%;text-align:left;border-radius:4px 4px 0 0;border-collapse:separate;border-spacing:0}
.ant-table-layout-fixed table{table-layout:fixed} .ant-table-layout-fixed table{table-layout:fixed}
.ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:1px solid #e8e8e8;transition:background .3s ease} .ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:2px solid rgba(155, 155, 155, 0.15);transition:background .3s ease}
.ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center} .ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center}
.ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;transition:all .3s} .ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;transition:all .3s}
.ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px} .ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px}
@@ -4051,7 +4052,7 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0} .ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0}
.ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8} .ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8}
.ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8} .ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8}
.ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px} .ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:2px solid rgba(155, 155, 155, 0.15);border-bottom:2px solid rgba(155, 155, 155, 0.15);border-radius:0 0 4px 4px}
.ant-table-pagination.ant-pagination{float:right;margin:16px 0} .ant-table-pagination.ant-pagination{float:right;margin:16px 0}
.ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.15)} .ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:4px 4px 0 0;box-shadow:none} .ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:4px 4px 0 0;box-shadow:none}
@@ -4074,8 +4075,8 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer} .ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer}
.ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)} .ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)}
.ant-table-row-expand-icon{color:#1890ff;text-decoration:none;cursor:pointer;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:0;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .ant-table-row-expand-icon{color:#1890ff;text-decoration:none;cursor:pointer;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:0;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:#40a9ff} .ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:rgb(0 150 112)}
.ant-table-row-expand-icon:active{color:#096dd9} .ant-table-row-expand-icon:active{color:rgb(0 150 112)}
.ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor} .ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor}
.ant-table-row-expanded:after{content:"-"} .ant-table-row-expanded:after{content:"-"}
.ant-table-row-collapsed:after{content:"+"} .ant-table-row-collapsed:after{content:"+"}
@@ -4493,4 +4494,4 @@ to{width:0;height:0;margin:0;padding:0;opacity:0}
} }
@keyframes uploadAnimateInlineOut{ @keyframes uploadAnimateInlineOut{
to{width:0;height:0;margin:0;padding:0;opacity:0} to{width:0;height:0;margin:0;padding:0;opacity:0}
} }

View File

@@ -1,5 +1,22 @@
#app { html,
body {
height: 100vh; height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
}
#app {
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
overflow: auto;
} }
.ant-space { .ant-space {
@@ -10,8 +27,14 @@
display: none; display: none;
} }
@media (max-width: 768px) {
.ant-layout-sider {
display: none;
}
}
.ant-card { .ant-card {
border-radius: 10px; border-radius: 1.5rem;
} }
.ant-card-hoverable { .ant-card-hoverable {
@@ -180,13 +203,35 @@
.ant-card-dark:hover { .ant-card-dark:hover {
border-color: #e8e8e8; border-color: #e8e8e8;
/* box-shadow: 0 3px 12px -0.8px #0000005c; */ box-shadow: 0 1px 10px -1px rgb(154 175 238 / 70%);
} }
.ant-card-bordered:hover { .ant-setting-textarea {
box-shadow: 0 3px 12px -0.8px #0000005c; margin-top: 1.5rem;
min-height: 300px !important;
/*max-height: 800px !important;*/
} }
.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;
/*background-color: rgb(36 44 58 / 50%);*/
}
.ant-card-dark-securitybox-nohover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-securitybox-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
/* .ant-card-bordered:hover {
box-shadow: 0 3px 12px -0.8px #0000005c;
} */
.ant-card-dark .ant-table-thead th { .ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;
@@ -203,6 +248,7 @@
.ant-card-dark .ant-input-group-addon { .ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #262f3d; background-color: #262f3d;
border: 1px solid rgb(149 149 149 / 30%);
} }
.ant-card-dark .ant-list-item-meta-title, .ant-card-dark .ant-list-item-meta-title,
@@ -226,14 +272,17 @@
color: hsla(0,0%,100%,.65); 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-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; background-color: #11314d;
} }
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: rgb(4, 119, 90);
}
.ant-card-dark tbody .ant-table-expanded-row, .ant-card-dark tbody .ant-table-expanded-row,
.ant-card-dark .ant-calendar-time-picker-inner { .ant-card-dark .ant-calendar-time-picker-inner {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
@@ -256,6 +305,15 @@
border: 1px solid rgb(0 150 112 / 0%); border: 1px solid rgb(0 150 112 / 0%);
} }
.ant-layout:not(.login) .ant-input:focus,
.ant-layout:not(.login) .ant-input:hover,
.ant-layout:not(.login) .ant-input-number:focus,
.ant-layout:not(.login) .ant-input-number:hover,
.ant-layout:not(.login) .ant-calendar-input:focus,
.ant-layout:not(.login) .ant-calendar-input:hover {
background-color: rgba(0, 149, 111, 0.1);
}
.ant-card-dark .ant-select-disabled .ant-select-selection { .ant-card-dark .ant-select-disabled .ant-select-selection {
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
background-color: #242c3a; background-color: #242c3a;
@@ -264,11 +322,12 @@
.ant-card-dark .ant-collapse-item { .ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;
border-radius: 0.5rem 0.5rem 0 0;
} }
.ant-dropdown-menu-dark, .ant-dropdown-menu-dark,
.ant-card-dark .ant-modal-content { .ant-card-dark .ant-modal-content {
border: 1px solid rgba(255, 255, 255, 0.65); border:1px solid rgb(100 100 100 / 20%);
box-shadow: 0 2px 8px rgba(255,255,255,.15); box-shadow: 0 2px 8px rgba(255,255,255,.15);
} }
@@ -280,7 +339,7 @@
} }
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date { .ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: #1668dc; background-color: rgb(0 150 112);
} }
.ant-card-dark .ant-calendar-time-picker-select li:hover { .ant-card-dark .ant-calendar-time-picker-select li:hover {
@@ -333,9 +392,9 @@
} }
.ant-card-dark .ant-tag-green { .ant-card-dark .ant-tag-green {
color: #6abe39; color: #37b998;
background: #162312; background: #101e1a;
border-color: #274916; border-color: #144237;
} }
.ant-card-dark .ant-tag-cyan { .ant-card-dark .ant-tag-cyan {
@@ -362,7 +421,7 @@
} }
.ant-card-dark .ant-switch-checked { .ant-card-dark .ant-switch-checked {
background-color: #0c61b0; background-color: #009670;
} }
.ant-card-dark .ant-btn, .ant-card-dark .ant-btn,
@@ -373,19 +432,19 @@
} }
.ant-card-dark .ant-radio-button-wrapper:hover { .ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc; color: #009670;
} }
.ant-card-dark .ant-btn-primary { .ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #073763; background-color: rgb(7 98 75 / 50%);
border-color: #1890ff; border-color: #009670;
text-shadow: 0 -1px 0 rgba(255,255,255,.12); text-shadow: 0 -1px 0 rgba(255,255,255,.12);
box-shadow: 0 2px 0 rgba(255,255,255,.045); box-shadow: 0 2px 0 rgba(255,255,255,.045);
} }
.ant-card-dark .ant-btn-primary:hover { .ant-card-dark .ant-btn-primary:hover {
background-color: #40a9ff; background-color: #009670;
border-color: #40a9ff; border-color: #40a9ff00;
} }
.ant-dark .ant-popover-content { .ant-dark .ant-popover-content {
@@ -408,15 +467,18 @@
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 0.7em;
}
::-webkit-scrollbar-track {
background: rgb(50 62 82 / 25%);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #11141a57; background: rgb(133 133 133 / 80%);
border-radius: 20px; border-radius: 100vw;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #11141a66; background: #919191;
border-radius: 20px; }
}

View File

@@ -2,7 +2,7 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use( axios.interceptors.request.use(
config => { (config) => {
if (config.data instanceof FormData) { if (config.data instanceof FormData) {
config.headers['Content-Type'] = 'multipart/form-data'; config.headers['Content-Type'] = 'multipart/form-data';
} else { } else {
@@ -12,5 +12,5 @@ axios.interceptors.request.use(
} }
return config; return config;
}, },
error => Promise.reject(error) (error) => Promise.reject(error),
); );

View File

@@ -1,36 +1,41 @@
supportLangs = [ const supportLangs = [
{ {
name : "English", name: 'English',
value : "en-US", value: 'en-US',
icon : "🇺🇸" icon: '🇺🇸',
}, },
{ {
name : "Farsi", name: 'فارسی',
value : "fa_IR", value: 'fa_IR',
icon : "🇮🇷" icon: '🇮🇷',
}, },
{ {
name : "汉语", name: '汉语',
value : "zh-Hans", value: 'zh-Hans',
icon : "🇨🇳" icon: '🇨🇳',
}, },
] {
name: 'Русский',
value: 'ru_RU',
icon: '🇷🇺',
},
];
function getLang(){ function getLang() {
let lang = getCookie('lang') let lang = getCookie('lang');
if (! lang){ if (!lang) {
if (window.navigator){ if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage; lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)){ if (isSupportLang(lang)) {
setCookie('lang' , lang , 150) setCookie('lang', lang, 150);
}else{ } else {
setCookie('lang' , 'en-US' , 150) setCookie('lang', 'en-US', 150);
window.location.reload(); window.location.reload();
} }
}else{ } else {
setCookie('lang' , 'en-US' , 150) setCookie('lang', 'en-US', 150);
window.location.reload(); window.location.reload();
} }
} }
@@ -38,47 +43,21 @@ function getLang(){
return lang; return lang;
} }
function setLang(lang){ function setLang(lang) {
if (!isSupportLang(lang)) {
if (!isSupportLang(lang)){
lang = 'en-US'; lang = 'en-US';
} }
setCookie('lang' , lang , 150) setCookie('lang', lang, 150);
window.location.reload(); window.location.reload();
} }
function isSupportLang(lang){ function isSupportLang(lang) {
for (l of supportLangs){ for (l of supportLangs) {
if (l.value === lang){ if (l.value === lang) {
return true; return true;
} }
} }
return false; return false;
} }
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -482,8 +482,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.settings = settings; this.settings = settings;
} }
addCert(cert) { addCert() {
this.certs.push(cert); this.certs.push(new TlsStreamSettings.Cert());
} }
removeCert(index) { removeCert(index) {
@@ -599,8 +599,8 @@ class XtlsStreamSettings extends XrayCommonClass {
this.settings = settings; this.settings = settings;
} }
addCert(cert) { addCert() {
this.certs.push(cert); this.certs.push(new XtlsStreamSettings.Cert());
} }
removeCert(index) { removeCert(index) {
@@ -725,7 +725,7 @@ class RealityStreamSettings extends XrayCommonClass {
static fromJson(json = {}) { static fromJson(json = {}) {
let settings; let settings;
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName); settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName, json.settings.spiderX);
} }
return new RealityStreamSettings( return new RealityStreamSettings(
json.show, json.show,
@@ -758,17 +758,19 @@ class RealityStreamSettings extends XrayCommonClass {
} }
RealityStreamSettings.Settings = class extends XrayCommonClass { RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '') { constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
super(); super();
this.publicKey = publicKey; this.publicKey = publicKey;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.serverName = serverName; this.serverName = serverName;
this.spiderX = spiderX;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new RealityStreamSettings.Settings( return new RealityStreamSettings.Settings(
json.publicKey, json.publicKey,
json.fingerprint, json.fingerprint,
json.serverName, json.serverName,
json.spiderX,
); );
} }
toJson() { toJson() {
@@ -776,6 +778,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
publicKey: this.publicKey, publicKey: this.publicKey,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
serverName: this.serverName, serverName: this.serverName,
spiderX: this.spiderX,
}; };
} }
}; };
@@ -1370,6 +1373,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName; address = this.stream.reality.settings.serverName;
} }
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
} }
const link = `vless://${uuid}@${address}:${port}`; const link = `vless://${uuid}@${address}:${port}`;
@@ -1470,6 +1476,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) { if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName; address = this.stream.reality.settings.serverName;
} }
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
} }
if (this.xtls) { if (this.xtls) {
@@ -1487,7 +1496,7 @@ class Inbound extends XrayCommonClass {
params.set("flow", this.settings.trojans[clientIndex].flow); params.set("flow", this.settings.trojans[clientIndex].flow);
} }
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`; const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
url.searchParams.set(key, value) url.searchParams.set(key, value)

View File

@@ -56,14 +56,61 @@ function toFixed(num, n) {
return Math.round(num * n) / n; return Math.round(num * n) / n;
} }
function debounce (fn, delay) { function debounce(fn, delay) {
var timeoutID = null var timeoutID = null;
return function () { return function () {
clearTimeout(timeoutID) clearTimeout(timeoutID);
var args = arguments var args = arguments;
var that = this var that = this;
timeoutID = setTimeout(function () { timeoutID = setTimeout(function () {
fn.apply(that, args) fn.apply(that, args);
}, delay) }, delay);
};
}
function getCookie(cname) {
let name = cname + '=';
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
} }
} return '';
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
}
function usageColor(data, threshold, total) {
switch (true) {
case data === null:
return 'blue';
case total <= 0:
return 'blue';
case data < total - threshold:
return 'cyan';
case data < total:
return 'orange';
default:
return 'red';
}
}
function doAllItemsExist(array1, array2) {
for (let i = 0; i < array1.length; i++) {
if (!array2.includes(array1[i])) {
return false;
}
}
return true;
}

View File

@@ -1,67 +1,67 @@
const oneMinute = 1000 * 60; // 一分钟的毫秒数 const oneMinute = 1000 * 60; // MilliseConds in a Minute
const oneHour = oneMinute * 60; // 一小时的毫秒数 const oneHour = oneMinute * 60; // The milliseconds of one hour
const oneDay = oneHour * 24; // 一天的毫秒数 const oneDay = oneHour * 24; // The Number of MilliseConds A Day
const oneWeek = oneDay * 7; // 一星期的毫秒数 const oneWeek = oneDay * 7; // The milliseconds per week
const oneMonth = oneDay * 30; // 一个月的毫秒数 const oneMonth = oneDay * 30; // The milliseconds of a month
/** /**
* 按天数减少 * Decrease according to the number of days
* *
* @param days 要减少的天数 * @param days to reduce the number of days to be reduced
*/ */
Date.prototype.minusDays = function (days) { Date.prototype.minusDays = function (days) {
return this.minusMillis(oneDay * days); return this.minusMillis(oneDay * days);
}; };
/** /**
* 按天数增加 * Increase according to the number of days
* *
* @param days 要增加的天数 * @param days The number of days to be increased
*/ */
Date.prototype.plusDays = function (days) { Date.prototype.plusDays = function (days) {
return this.plusMillis(oneDay * days); return this.plusMillis(oneDay * days);
}; };
/** /**
* 按小时减少 * A few
* *
* @param hours 要减少的小时数 * @param hours to be reduced
*/ */
Date.prototype.minusHours = function (hours) { Date.prototype.minusHours = function (hours) {
return this.minusMillis(oneHour * hours); return this.minusMillis(oneHour * hours);
}; };
/** /**
* 按小时增加 * Increase hourly
* *
* @param hours 要增加的小时数 * @param hours to increase the number of hours
*/ */
Date.prototype.plusHours = function (hours) { Date.prototype.plusHours = function (hours) {
return this.plusMillis(oneHour * hours); return this.plusMillis(oneHour * hours);
}; };
/** /**
* 按分钟减少 * Make reduction in minutes
* *
* @param minutes 要减少的分钟数 * @param minutes to reduce the number of minutes
*/ */
Date.prototype.minusMinutes = function (minutes) { Date.prototype.minusMinutes = function (minutes) {
return this.minusMillis(oneMinute * minutes); return this.minusMillis(oneMinute * minutes);
}; };
/** /**
* 按分钟增加 * Add in minutes
* *
* @param minutes 要增加的分钟数 * @param minutes to increase the number of minutes
*/ */
Date.prototype.plusMinutes = function (minutes) { Date.prototype.plusMinutes = function (minutes) {
return this.plusMillis(oneMinute * minutes); return this.plusMillis(oneMinute * minutes);
}; };
/** /**
* 按毫秒减少 * Decrease in milliseconds
* *
* @param millis 要减少的毫秒数 * @param millis to reduce the milliseconds
*/ */
Date.prototype.minusMillis = function(millis) { Date.prototype.minusMillis = function(millis) {
let time = this.getTime() - millis; let time = this.getTime() - millis;
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
}; };
/** /**
* 按毫秒增加 * Add in milliseconds to increase
* *
* @param millis 要增加的毫秒数 * @param millis to increase the milliseconds to increase
*/ */
Date.prototype.plusMillis = function(millis) { Date.prototype.plusMillis = function(millis) {
let time = this.getTime() + millis; let time = this.getTime() + millis;
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
}; };
/** /**
* 设置时间为当天的 00:00:00.000 * Setting time is 00: 00: 00.000 on the day
*/ */
Date.prototype.setMinTime = function () { Date.prototype.setMinTime = function () {
this.setHours(0); this.setHours(0);
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
}; };
/** /**
* 设置时间为当天的 23:59:59.999 * Setting time is 23: 59: 59.999 on the same day
*/ */
Date.prototype.setMaxTime = function () { Date.prototype.setMaxTime = function () {
this.setHours(23); this.setHours(23);
@@ -105,37 +105,36 @@ Date.prototype.setMaxTime = function () {
}; };
/** /**
* 格式化日期 * Formatting date
*/ */
Date.prototype.formatDate = function () { Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate()); return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
}; };
/** /**
* 格式化时间 * Format time
*/ */
Date.prototype.formatTime = function () { Date.prototype.formatTime = function () {
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds()); return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
}; };
/** /**
* 格式化日期加时间 * Formatting date plus time
* *
* @param split 日期和时间之间的分隔符,默认是一个空格 * @param split Date and time separation symbols, default is a space
*/ */
Date.prototype.formatDateTime = function (split = ' ') { Date.prototype.formatDateTime = function (split = ' ') {
return this.formatDate() + split + this.formatTime(); return this.formatDate() + split + this.formatTime();
}; };
class DateUtil { class DateUtil {
// String string to date object
// 字符串转 Date 对象
static parseDate(str) { static parseDate(str) {
return new Date(str.replace(/-/g, '/')); return new Date(str.replace(/-/g, '/'));
} }
static formatMillis(millis) { static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s') return moment(millis).format('YYYY-M-D H:m:s');
} }
static firstDayOfMonth() { static firstDayOfMonth() {
@@ -144,4 +143,4 @@ class DateUtil {
date.setMinTime(); date.setMinTime();
return date; return date;
} }
} }

View File

@@ -68,13 +68,11 @@ class HttpUtil {
} }
class PromiseUtil { class PromiseUtil {
static async sleep(timeout) { static async sleep(timeout) {
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, timeout) setTimeout(resolve, timeout)
}); });
} }
} }
const seq = [ const seq = [
@@ -95,7 +93,6 @@ const shortIdSeq = [
]; ];
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
return parseInt(Math.random() * (max - min) + min, 10); return parseInt(Math.random() * (max - min) + min, 10);
} }
@@ -153,8 +150,7 @@ class RandomUtil {
static randomText() { static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';
var len = 6 + Math.floor(Math.random() * 5) for (var ii = 0; ii < 8; ii++) {
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)]; string += chars[Math.floor(Math.random() * chars.length)];
} }
return string; return string;
@@ -162,11 +158,11 @@ class RandomUtil {
static randowShortId() { static randowShortId() {
let str = ''; let str = '';
str += this.randomShortIdSeq(8) str += this.randomShortIdSeq(8);
return str; return str;
} }
static randomShadowsocksPassword(){ static randomShadowsocksPassword() {
let array = new Uint8Array(32); let array = new Uint8Array(32);
window.crypto.getRandomValues(array); window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array)); return btoa(String.fromCharCode.apply(null, array));
@@ -174,7 +170,6 @@ class RandomUtil {
} }
class ObjectUtil { class ObjectUtil {
static getPropIgnoreCase(obj, prop) { static getPropIgnoreCase(obj, prop) {
for (const name in obj) { for (const name in obj) {
if (!obj.hasOwnProperty(name)) { if (!obj.hasOwnProperty(name)) {
@@ -322,5 +317,4 @@ class ObjectUtil {
} }
return true; return true;
} }
} }

View File

@@ -14,7 +14,7 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
} }
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui/API/inbounds") g = g.Group("/panel/api/inbounds")
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.GET("/list", a.getAllInbounds) g.GET("/list", a.getAllInbounds)
@@ -35,21 +35,27 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
} }
func (a *APIController) getAllInbounds(c *gin.Context) { func (a *APIController) getAllInbounds(c *gin.Context) {
a.inboundController.getInbounds(c) a.inboundController.getInbounds(c)
} }
func (a *APIController) getSingleInbound(c *gin.Context) { func (a *APIController) getSingleInbound(c *gin.Context) {
a.inboundController.getInbound(c) a.inboundController.getInbound(c)
} }
func (a *APIController) getClientTraffics(c *gin.Context) { func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c) a.inboundController.getClientTraffics(c)
} }
func (a *APIController) addInbound(c *gin.Context) { func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c) a.inboundController.addInbound(c)
} }
func (a *APIController) delInbound(c *gin.Context) { func (a *APIController) delInbound(c *gin.Context) {
a.inboundController.delInbound(c) a.inboundController.delInbound(c)
} }
func (a *APIController) updateInbound(c *gin.Context) { func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(c) a.inboundController.updateInbound(c)
} }
@@ -61,24 +67,31 @@ func (a *APIController) getClientIps(c *gin.Context) {
func (a *APIController) clearClientIps(c *gin.Context) { func (a *APIController) clearClientIps(c *gin.Context) {
a.inboundController.clearClientIps(c) a.inboundController.clearClientIps(c)
} }
func (a *APIController) addInboundClient(c *gin.Context) { func (a *APIController) addInboundClient(c *gin.Context) {
a.inboundController.addInboundClient(c) a.inboundController.addInboundClient(c)
} }
func (a *APIController) delInboundClient(c *gin.Context) { func (a *APIController) delInboundClient(c *gin.Context) {
a.inboundController.delInboundClient(c) a.inboundController.delInboundClient(c)
} }
func (a *APIController) updateInboundClient(c *gin.Context) { func (a *APIController) updateInboundClient(c *gin.Context) {
a.inboundController.updateInboundClient(c) a.inboundController.updateInboundClient(c)
} }
func (a *APIController) resetClientTraffic(c *gin.Context) { func (a *APIController) resetClientTraffic(c *gin.Context) {
a.inboundController.resetClientTraffic(c) a.inboundController.resetClientTraffic(c)
} }
func (a *APIController) resetAllTraffics(c *gin.Context) { func (a *APIController) resetAllTraffics(c *gin.Context) {
a.inboundController.resetAllTraffics(c) a.inboundController.resetAllTraffics(c)
} }
func (a *APIController) resetAllClientTraffics(c *gin.Context) { func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c) a.inboundController.resetAllClientTraffics(c)
} }
func (a *APIController) delDepletedClients(c *gin.Context) { func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c) a.inboundController.delDepletedClients(c)
} }

View File

@@ -39,7 +39,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
func (a *IndexController) index(c *gin.Context) { func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) { if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "xui/") c.Redirect(http.StatusTemporaryRedirect, "panel/")
return return
} }
html(c, "login.html", "pages.login.title", nil) html(c, "login.html", "pages.login.title", nil)
@@ -77,9 +77,11 @@ func (a *IndexController) login(c *gin.Context) {
logger.Infof("Unable to get session's max age from DB") logger.Infof("Unable to get session's max age from DB")
} }
err = session.SetMaxAge(c, sessionMaxAge*60) if sessionMaxAge > 0 {
if err != nil { err = session.SetMaxAge(c, sessionMaxAge*60)
logger.Infof("Unable to set session's max age") if err != nil {
logger.Infof("Unable to set session's max age")
}
} }
err = session.SetLoginUser(c, user) err = session.SetLoginUser(c, user)
@@ -101,5 +103,4 @@ func (a *IndexController) getSecretStatus(c *gin.Context) {
if err == nil { if err == nil {
jsonObj(c, status, nil) jsonObj(c, status, nil)
} }
} }

View File

@@ -1,6 +1,9 @@
package controller package controller
import ( import (
"fmt"
"net/http"
"regexp"
"time" "time"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
@@ -8,6 +11,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
type ServerController struct { type ServerController struct {
BaseController BaseController
@@ -136,14 +141,27 @@ func (a *ServerController) getDb(c *gin.Context) {
jsonMsg(c, "get Database", err) jsonMsg(c, "get Database", err)
return return
} }
filename := "x-ui.db"
if !isValidFilename(filename) {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
return
}
// Set the headers for the response // Set the headers for the response
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=x-ui.db") c.Header("Content-Disposition", "attachment; filename="+filename)
// Write the file contents to the response // Write the file contents to the response
c.Writer.Write(db) c.Writer.Write(db)
} }
func isValidFilename(filename string) bool {
// Validate that the filename only contains allowed characters
return filenameRegex.MatchString(filename)
}
func (a *ServerController) importDB(c *gin.Context) { func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body // Get the file from the request body
file, _, err := c.Request.FormFile("db") file, _, err := c.Request.FormFile("db")

View File

@@ -149,6 +149,7 @@ func (a *SettingController) updateSecret(c *gin.Context) {
} }
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
} }
func (a *SettingController) getUserSecret(c *gin.Context) { func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c) loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id) user := a.userService.GetUserSecret(loginUser.Id)

View File

@@ -29,7 +29,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) { func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0] host := strings.Split(c.Request.Host, ":")[0]
subs, header, err := a.subService.GetSubs(subId, host) subs, headers, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
@@ -38,8 +38,10 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n" result += sub + "\n"
} }
// Add subscription-userinfo // Add headers
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} }

View File

@@ -18,7 +18,7 @@ func NewXUIController(g *gin.RouterGroup) *XUIController {
} }
func (a *XUIController) initRouter(g *gin.RouterGroup) { func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui") g = g.Group("/panel")
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.GET("/", a.index) g.GET("/", a.index)

View File

@@ -1,7 +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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> :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"
@@ -36,12 +36,12 @@
}, },
confirm() {}, confirm() {},
open({ open({
title='', title = '',
type='text', type = 'text',
value='', value = '',
okText='{{ i18n "sure"}}', okText = '{{ i18n "sure"}}',
confirm=() => {}, confirm = () => {},
}) { }) {
this.title = title; this.title = title;
this.type = type; this.type = type;
this.value = value; this.value = value;

View File

@@ -1,11 +1,16 @@
{{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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="300px"> width="300px">
<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;">
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas> {{ i18n "pages.inbounds.clickOnQRcode" }}
</a-tag>
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%; margin-top: 10px;"></canvas>
</a-modal> </a-modal>
<script> <script>
@@ -16,14 +21,16 @@
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
copyText: '', copyText: '',
clientName: null,
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') { show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clientName = clientName;
if (ObjectUtil.isEmpty(copyText)) { if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content; this.copyText = content;
} else { } else {
@@ -48,6 +55,7 @@
}; };
const qrModalApp = new Vue({ const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal', el: '#qrcode-modal',
data: { data: {
qrModal: qrModal, qrModal: qrModal,

View File

@@ -1,7 +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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> :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)"
@@ -21,7 +21,7 @@
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', fileName='') { show: function (title = '', content = '', fileName = '') {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.fileName = fileName; this.fileName = fileName;

View File

@@ -18,6 +18,12 @@
border-radius: 30px; border-radius: 30px;
} }
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
}
.ant-input-affix-wrapper .ant-input-prefix { .ant-input-affix-wrapper .ant-input-prefix {
left: 23px; left: 23px;
} }
@@ -26,20 +32,26 @@
padding-left: 50px; padding-left: 50px;
} }
.selectLang{ .centered {
display: flex; display: flex;
text-align: center; text-align: center;
align-items: center;
justify-content: center; justify-content: center;
} }
.title {
font-size: 32px;
font-weight: bold;
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak class="login" :class="themeSwitcher.darkCardClass">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content> <a-layout-content>
<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 :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1>{{ 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">
@@ -48,35 +60,33 @@
<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="color: rgba(0,0,0,.25)"/> <a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-input type="password" v-model.trim="user.password" <password-input icon="lock" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/> </password-input>
</a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="secretEnable"> <a-form-item v-if="secretEnable">
<a-input type="text" placeholder='{{ i18n "secretToken" }}' v-model.trim="user.loginSecret" @keydown.enter.native="login"> <password-input icon="key" v-model.trim="user.loginSecret"
<a-icon slot="prefix" type="key" style="color: rgba(0,0,0,.25)"/> placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
</password-input>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button> <a-row justify="center" class="centered">
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</a-row>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered">
<a-row justify="center" class="selectLang"> <a-col :span="12">
<a-col :span="5"><span>Language :</span></a-col> <a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<a-col :span="7">
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
>
<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>
</a-select-option> </a-select-option>
@@ -84,6 +94,11 @@
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch />
</a-row>
</a-form-item>
</a-form> </a-form>
</a-col> </a-col>
</a-row> </a-row>
@@ -91,24 +106,24 @@
</transition> </transition>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
<script> <script>
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = background;
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
themeSwitcher,
loading: false, loading: false,
user: new User(), user: new User(),
secretEnable: false, secretEnable: false,
lang : "" lang: ""
}, },
created(){ async created() {
this.lang = getLang(); this.updateBackground();
this.secretEnable = this.getSecretStatus(); this.lang = getLang();
this.secretEnable = await this.getSecretStatus();
}, },
methods: { methods: {
async login() { async login() {
@@ -116,20 +131,33 @@
const msg = await HttpUtil.post('/login', this.user); const msg = await HttpUtil.post('/login', this.user);
this.loading = false; this.loading = false;
if (msg.success) { if (msg.success) {
location.href = basePath + 'xui/'; location.href = basePath + 'panel/';
} }
}, },
async getSecretStatus() { async getSecretStatus() {
this.loading= true; this.loading = true;
const msg = await HttpUtil.post('/getSecretStatus'); const msg = await HttpUtil.post('/getSecretStatus');
this.loading = false; this.loading = false;
if (msg.success){ if (msg.success) {
this.secretEnable = msg.obj; this.secretEnable = msg.obj;
return msg.obj; return msg.obj;
} }
} },
} updateBackground() {
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = this.themeSwitcher.isDarkTheme ? colors.dark.bg : background;
},
},
watch: {
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
this.updateBackground();
},
},
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,11 @@
{{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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'> <a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :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>
@@ -71,20 +71,20 @@
</a-form-item> </a-form-item>
<br> <br>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow"> <a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline"> <a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -104,7 +104,7 @@
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -113,7 +113,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker> v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -143,37 +143,42 @@
delayedStart: false, delayedStart: false,
ok() { ok() {
clients = []; clients = [];
method=clientsBulkModal.emailMethod; method = clientsBulkModal.emailMethod;
if(method>1){ if (method > 1) {
start=clientsBulkModal.firstNum; start = clientsBulkModal.firstNum;
end=clientsBulkModal.lastNum + 1; end = clientsBulkModal.lastNum + 1;
} else { } else {
start=0; start = 0;
end=clientsBulkModal.quantity; end = clientsBulkModal.quantity;
} }
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : ""; prefix = (method > 0 && clientsBulkModal.emailPrefix.length > 0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1); useNum = (method > 1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : ""; postfix = (method > 2 && clientsBulkModal.emailPostfix.length > 0) ? clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol); newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==4) newClient.email = ""; if (method == 4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix; newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId; newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId; newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp; newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB; newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime; newClient._expiryTime = clientsBulkModal.expiryTime;
if(clientsBulkModal.inbound.canEnableTlsFlow()){ if (clientsBulkModal.inbound.canEnableTlsFlow()) {
newClient.flow = clientsBulkModal.flow; newClient.flow = clientsBulkModal.flow;
} }
if(clientsBulkModal.inbound.xtls){ if (clientsBulkModal.inbound.xtls) {
newClient.flow = clientsBulkModal.flow; newClient.flow = clientsBulkModal.flow;
} }
clients.push(newClient); clients.push(newClient);
} }
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id); ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
}, },
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) { show({
title = '',
okText = '{{ i18n "sure" }}',
dbInbound = null,
confirm = (inbound, dbInbound) => { }
}) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
@@ -181,21 +186,21 @@
this.quantity = 1; this.quantity = 1;
this.totalGB = 0; this.totalGB = 0;
this.expiryTime = 0; this.expiryTime = 0;
this.emailMethod= 0; this.emailMethod = 0;
this.limitIp= 0; this.limitIp = 0;
this.firstNum= 1; this.firstNum = 1;
this.lastNum= 1; this.lastNum = 1;
this.emailPrefix= ""; this.emailPrefix = "";
this.emailPostfix= ""; this.emailPostfix = "";
this.subId= ""; this.subId = "";
this.tgId= ""; this.tgId = "";
this.flow= ""; this.flow = "";
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.delayedStart = false; this.delayedStart = false;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
@@ -230,10 +235,11 @@
get delayedExpireDays() { get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0; return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
}, },
set delayedExpireDays(days){ set delayedExpireDays(days) {
this.clientsBulkModal.expiryTime = -86400000 * days; this.clientsBulkModal.expiryTime = -86400000 * days;
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-modal> </a-modal>
@@ -20,16 +20,15 @@
oldClientId: "", oldClientId: "",
index: null, index: null,
clientIps: null, clientIps: null,
isExpired: false,
delayedStart: false, delayedStart: false,
ok() { ok() {
if(clientModal.isEdit){ if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId); ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else { } else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id); ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
} }
}, },
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) { show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
@@ -38,13 +37,12 @@
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings); this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index; this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false; this.delayedStart = false;
if (isEdit){ if (isEdit) {
if (this.clients[index].expiryTime < 0){ if (this.clients[index].expiryTime < 0) {
this.delayedStart = true; this.delayedStart = true;
} }
this.oldClientId = this.getClientId(dbInbound.protocol,clients[index]); this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else { } else {
this.addClient(this.inbound.protocol, this.clients); this.addClient(this.inbound.protocol, this.clients);
} }
@@ -52,7 +50,7 @@
this.confirm = confirm; this.confirm = confirm;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
@@ -61,7 +59,7 @@
} }
}, },
getClientId(protocol, client) { getClientId(protocol, client) {
switch(protocol){ switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email; case Protocols.SHADOWSOCKS: return client.email;
default: return client.id; default: return client.id;
@@ -103,39 +101,27 @@
return this.clientModal.isEdit; return this.clientModal.isEdit;
}, },
get isTrafficExhausted() { get isTrafficExhausted() {
if(!clientStats) return false if (!clientStats) return false
if(clientStats.total <= 0) return false if (clientStats.total <= 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false if (clientStats.up + clientStats.down < clientStats.total) return false
return true return true
}, },
get isExpiry() { get isExpiry() {
return this.clientModal.isExpired return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
}, },
get statsColor() { get statsColor() {
if(!clientStats) return 'blue' return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
if(clientStats.total <= 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
}, },
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;
}, },
set delayedExpireDays(days){ set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days; this.client.expiryTime = -86400000 * days;
}, },
}, },
methods: { methods: {
getNewEmail(client) { async getDBClientIps(email, event) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; const msg = await HttpUtil.post('/panel/inbound/clientIps/' + email);
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
async getDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -149,22 +135,22 @@
} }
}, },
async clearDBClientIps(email) { async clearDBClientIps(email) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email); const msg = await HttpUtil.post('/panel/inbound/clearClientIps/' + email);
if (!msg.success) { if (!msg.success) {
return; return;
} }
document.getElementById("clientIPs").value = "" document.getElementById("clientIPs").value = ""
}, },
resetClientTraffic(email,dbInboundId,iconElement) { resetClientTraffic(email, dbInboundId, iconElement) {
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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email); const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) { if (msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;
@@ -175,5 +161,6 @@
}, },
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,17 +1,17 @@
{{define "menuItems"}} {{define "menuItems"}}
<a-menu-item key="{{ .base_path }}xui/"> <a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon> <a-icon type="dashboard"></a-icon>
<span>{{ i18n "menu.dashboard"}}</span> <span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}xui/inbounds"> <a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon> <a-icon type="user"></a-icon>
<span>{{ i18n "menu.inbounds"}}</span> <span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}xui/settings"> <a-menu-item key="{{ .base_path }}panel/settings">
<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/clients">--> <!--<a-menu-item key="{{ .base_path }}panel/clients">-->
<!-- <a-icon type="laptop"></a-icon>--> <!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>--> <!-- <span>Client</span>-->
<!--</a-menu-item>--> <!--</a-menu-item>-->
@@ -23,17 +23,14 @@
{{define "commonSider"}} {{define "commonSider"}}
<a-layout-sider :theme="siderDrawer.theme" 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="siderDrawer.theme" 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="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme" <theme-switch />
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
@@ -41,32 +38,25 @@
<a-drawer id="sider-drawer" placement="left" :closable="false" <a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()" @close="siderDrawer.close()"
:visible="siderDrawer.visible" :visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''" :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="siderDrawer.theme" 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="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme" <theme-switch />
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-drawer> </a-drawer>
<script> <script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = { const siderDrawer = {
visible: false, visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'false' ? false : true,
show() { show() {
this.visible = true; this.visible = true;
}, },
@@ -76,17 +66,6 @@
change() { change() {
this.visible = !this.visible; this.visible = !this.visible;
}, },
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
}
}; };
</script> </script>
{{end}} {{end}}

View File

@@ -0,0 +1,35 @@
{{define "component/passwordInput"}}
<template>
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
:placeholder="placeholder"
@input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword"
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
</a-input>
</template>
{{end}}
{{define "component/password"}}
<script>
Vue.component('password-input', {
props: ["title", "value", "placeholder", "icon"],
template: `{{template "component/passwordInput"}}`,
data() {
return {
showPassword: false,
};
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword;
},
},
});
</script>
{{end}}

View File

@@ -1,6 +1,13 @@
{{define "component/settingListItem"}} {{define "component/settingListItem"}}
<a-list-item style="padding: 20px"> <a-list-item style="padding: 20px">
<a-row> <a-row v-if="type === 'textarea'">
<a-col>
<a-list-item-meta :title="title" :description="desc"/>
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10 }"></a-textarea>
<!--a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 30 }"></a-textarea-->
</a-col>
</a-row>
<a-row v-else>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/> <a-list-item-meta :title="title" :description="desc"/>
</a-col> </a-col>
@@ -11,9 +18,6 @@
<template v-else-if="type === 'number'"> <template v-else-if="type === 'number'">
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number> <a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
</template> </template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
</template>
<template v-else-if="type === 'switch'"> <template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch> <a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
</template> </template>
@@ -29,4 +33,4 @@
template: `{{template "component/settingListItem"}}`, template: `{{template "component/settingListItem"}}`,
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -0,0 +1,58 @@
{{define "component/themeSwitchTemplate"}}
<template>
<a-switch :default-checked="themeSwitcher.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="themeSwitcher.toggleTheme()">
</a-switch>
</template>
{{end}}
{{define "component/themeSwitcher"}}
<script>
const colors = {
dark: {
bg: "#242c3a",
text: "hsla(0,0%,100%,.65)"
},
light: {
bg: '#f0f2f5',
text: "rgba(0, 0, 0, 0.7)",
}
}
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const theme = isDarkTheme ? 'dark' : 'light';
return {
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() {
return this.isDarkTheme ? 'dark' : 'light';
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.theme = this.isDarkTheme ? 'dark' : 'light';
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' : '';
},
};
}
const themeSwitcher = createThemeSwitcher();
Vue.component('theme-switch', {
props: [],
template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({ themeSwitcher }),
});
</script>
{{end}}

View File

@@ -1,7 +1,9 @@
{{define "form/client"}} {{define "form/client"}}
<a-form layout="inline" v-if="client"> <a-form layout="inline" v-if="client">
<template v-if="isEdit"> <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> <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>
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'> <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch> <a-switch v-model="client.enable"></a-switch>
@@ -9,25 +11,27 @@
<br> <br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 200px;" ></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS"> <a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon> <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;" ></a-input> <a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
</a-form-item> </a-form-item>
<br> <br>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS"> <a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input-number v-model="client.alterId"></a-input-number> <a-input-number v-model="client.alterId"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"> <a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
@@ -40,8 +44,9 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.subId"></a-input> <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
</a-form-item> <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<span slot="label"> <span slot="label">
Telegram ID Telegram ID
@@ -85,26 +90,29 @@
</a-tooltip> </a-tooltip>
</span> </span>
<a-form layout="block"> <a-form layout="block">
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }"> <a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email,$event)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea> </a-textarea>
</a-form> </a-form>
</a-form-item> </a-form-item>
<br> <br>
<a-form-item v-if="inbound.xtls" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow"> <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -112,9 +120,10 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="client._totalGB":min="0"></a-input-number> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<template v-if="isEdit && clientStats"> <template v-if="isEdit && clientStats">
<br><span> {{ i18n "usage" }}:</span> <br>
<span> {{ i18n "usage" }}:</span>
<a-tag :color="statsColor"> <a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] / [[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]] [[ sizeFormat(clientStats.down) ]]
@@ -122,7 +131,8 @@
</a-tag> </a-tag>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon> <a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
</a-form-item> </a-form-item>
@@ -136,7 +146,7 @@
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -145,7 +155,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag> <a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item> </a-form-item>

View File

@@ -8,7 +8,7 @@
<a-switch v-model="dbInbound.enable"></a-switch> <a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
@@ -31,7 +31,7 @@
<br> <br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -43,7 +43,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -52,7 +52,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker> v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -8,7 +8,7 @@
</a-form-item> </a-form-item>
<br> <br>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<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

@@ -4,15 +4,15 @@
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Password"> <a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon> <a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
@@ -28,8 +28,9 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.subId"></a-input> <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
</a-form-item> <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<span slot="label"> <span slot="label">
Telegram ID Telegram ID
@@ -57,7 +58,7 @@
<br> <br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -77,7 +78,7 @@
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -86,7 +87,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-collapse-panel> </a-collapse-panel>
@@ -106,7 +107,7 @@
</a-form> </a-form>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
@@ -114,7 +115,7 @@
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input> <a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<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

@@ -17,8 +17,7 @@
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.settings.udp" <a-form-item v-if="inbound.settings.udp" label="IP">
label="IP">
<a-input v-model.trim="inbound.settings.ip"></a-input> <a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -4,17 +4,18 @@
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Password"> <a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 150px;"></a-input> <a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
@@ -27,7 +28,8 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.subId"></a-input> <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<span slot="label"> <span slot="label">
@@ -55,14 +57,14 @@
</a-form-item> </a-form-item>
<br> <br>
<a-form-item v-if="inbound.xtls" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -82,7 +84,7 @@
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -91,7 +93,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-collapse-panel> </a-collapse-panel>
@@ -113,8 +115,7 @@
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-row> <a-row>
<a-button type="primary" size="small" <a-button type="primary" size="small" @click="inbound.settings.addTrojanFallback()">
@click="inbound.settings.addTrojanFallback()">
+ +
</a-button> </a-button>
</a-row> </a-row>

View File

@@ -4,18 +4,19 @@
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="ID"> <a-form-item label="ID">
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<span slot="label"> <span slot="label">
@@ -27,8 +28,9 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.subId"></a-input> <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
</a-form-item> <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<span slot="label"> <span slot="label">
Telegram ID Telegram ID
@@ -55,20 +57,20 @@
</a-form-item> </a-form-item>
<br> <br>
<a-form-item v-if="inbound.xtls" label="Flow"> <a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <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 value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow"> <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <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 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>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -88,7 +90,7 @@
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -97,7 +99,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-collapse-panel> </a-collapse-panel>
@@ -119,8 +121,7 @@
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-row> <a-row>
<a-button type="primary" size="small" <a-button type="primary" size="small" @click="inbound.settings.addFallback()">
@click="inbound.settings.addFallback()">
+ +
</a-button> </a-button>
</a-row> </a-row>

View File

@@ -4,15 +4,15 @@
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input> <a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item> </a-form-item>
<br> <br>
<a-form-item label='{{ i18n "additional" }} ID'> <a-form-item label='{{ i18n "additional" }} ID'>
@@ -20,6 +20,7 @@
</a-form-item> </a-form-item>
<br> <br>
<a-form-item label="ID"> <a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input> <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
@@ -32,8 +33,9 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="client.subId"></a-input> <a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
</a-form-item> <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<span slot="label"> <span slot="label">
Telegram ID Telegram ID
@@ -61,7 +63,7 @@
<br> <br>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -81,7 +83,7 @@
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -90,7 +92,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-collapse-panel> </a-collapse-panel>

View File

@@ -1,21 +1,21 @@
{{define "form/sniffing"}} {{define "form/sniffing"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
Sniffing Sniffing
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span> <span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch> <a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled"> <a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox> <a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group> </a-checkbox-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/streamKCP"}} {{define "form/streamKCP"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<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 (Camouflage Video Call)</a-select-option> <a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
<a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option> <a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>

View File

@@ -1,7 +1,7 @@
{{define "form/streamQUIC"}} {{define "form/streamQUIC"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
@@ -11,7 +11,7 @@
<a-input v-model.trim="inbound.stream.quic.key"></a-input> <a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<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 (camouflage video call)</a-select-option> <a-select-option value="srtp">srtp (camouflage video call)</a-select-option>
<a-select-option value="utp">utp (camouflage BT download)</a-select-option> <a-select-option value="utp">utp (camouflage BT download)</a-select-option>

View File

@@ -2,7 +2,7 @@
<!-- select stream network --> <!-- select stream network -->
<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" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.darkCardClass">
<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">WS</a-select-option> <a-select-option value="ws">WS</a-select-option>

View File

@@ -13,8 +13,7 @@
</a-form> </a-form>
<!-- tcp request --> <!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'" <a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item> </a-form-item>
@@ -28,8 +27,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row> <a-row>
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+ +
</a-button> </a-button>
</a-row> </a-row>
@@ -39,8 +37,7 @@
<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" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" <a-button size="small" @click="inbound.stream.tcp.request.removeHeader(index)">
@click="inbound.stream.tcp.request.removeHeader(index)">
- -
</a-button> </a-button>
</template> </template>
@@ -50,8 +47,7 @@
</a-form> </a-form>
<!-- tcp response --> <!-- tcp response -->
<a-form v-if="inbound.stream.tcp.type === 'http'" <a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input> <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item> </a-form-item>
@@ -63,8 +59,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-row> <a-row>
<a-button size="small" <a-button size="small" @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-button>
</a-row> </a-row>
@@ -74,8 +69,7 @@
<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" }}'> addon-before='{{ 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)">
@click="inbound.stream.tcp.response.removeHeader(index)">
- -
</a-button> </a-button>
</template> </template>

View File

@@ -10,8 +10,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<a-row> <a-row>
<a-button size="small" <a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">
@click="inbound.stream.ws.addHeader('Host', '')">
+ +
</a-button> </a-button>
</a-row> </a-row>
@@ -21,8 +20,7 @@
<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" }}'> addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter"> <template slot="addonAfter">
<a-button size="small" <a-button size="small" @click="inbound.stream.ws.removeHeader(index)">
@click="inbound.stream.ws.removeHeader(index)">
- -
</a-button> </a-button>
</template> </template>

View File

@@ -37,18 +37,18 @@
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input> <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="CipherSuites"> <a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass">
<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 in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MinVersion"> <a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MaxVersion"> <a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -57,7 +57,7 @@
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" <a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 170px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
@@ -70,27 +70,31 @@
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch> <a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "certificate" }}'> <template v-for="cert,index in inbound.stream.tls.certs">
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid"> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button> <a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button> <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
</a-radio-group> <a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-form-item> </a-radio-group>
<template v-if="inbound.stream.tls.certs[0].useFile"> <a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'> <a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <template v-if="cert.useFile">
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input> <a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
</a-form-item> <a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input> <a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input> <a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>
@@ -111,27 +115,31 @@
<a-form-item label="Allow insecure"> <a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch> <a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "certificate" }}'> <template v-for="cert,index in inbound.stream.xtls.certs">
<a-radio-group v-model="inbound.stream.xtls.certs[0].useFile" button-style="solid"> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button> <a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button> <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
</a-radio-group> <a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-form-item> </a-radio-group>
<template v-if="inbound.stream.xtls.certs[0].useFile"> <a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'> <a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
<a-input v-model.trim="inbound.stream.xtls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <template v-if="cert.useFile">
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input> <a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
</a-form-item> <a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
<a-button type="primary" icon="import" @click="setDefaultCertXtls">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].cert"></a-input> <a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].key"></a-input> <a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>
@@ -145,16 +153,16 @@
<a-form-item label="xVer"> <a-form-item label="xVer">
<a-input-number v-model="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input-number> <a-input-number v-model="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="uTLS" > <a-form-item label="uTLS">
<a-select v-model="inbound.stream.reality.settings.fingerprint" <a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
<a-form-item label='{{ i18n "domainName" }}'> <a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input> <a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="dest"> <a-form-item label="Dest">
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input> <a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Server Names"> <a-form-item label="Server Names">
@@ -163,13 +171,16 @@
<a-form-item label="ShortIds"> <a-form-item label="ShortIds">
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input> <a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="SpiderX">
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item>
<a-form-item label="Private Key"> <a-form-item label="Private Key">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input> <a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Public Key"> <a-form-item label="Public Key">
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input> <a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item > <a-form-item>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button> <a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -29,20 +29,21 @@
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag> <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-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag> <a-tag :color="statsColor(record, client.email)" @click="alert(usageColor(0,1024,512))">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<template v-if="client._totalGB > 0"> <template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag> <a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
</template> </template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</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">
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'"> <a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
[[ DateUtil.formatMillis(client._expiryTime) ]] [[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag> </a-tag>
</template> </template>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag> <a-tag v-else-if="client.expiryTime < 0" color="cyan">
[[ client._expiryTime ]] {{ i18n "pages.client.days" }}
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template> </template>
{{end}} {{end}}

View File

@@ -3,62 +3,66 @@
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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="600px" width="600px"
> >
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px; width: 100%;">
<tr><td> <tr>
<table> <td>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr> <table>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr> <tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr> <tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
</table> <tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
</td> </table>
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> </td>
<table> <td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<tr> <table>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td> <tr>
</tr> <td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2"> </tr>
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr> <template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr> <tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
</template> <tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<template v-if="inbound.isQuic"> </template>
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr> <template v-if="inbound.isQuic">
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr> <tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
</template> <tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
<template v-if="inbound.isKcp"> </template>
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr> <template v-if="inbound.isKcp">
</template> <tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
<template v-if="inbound.isGrpc"> </template>
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr> <template v-if="inbound.isGrpc">
</template> <tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
</table> <tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</td></tr> </template>
<tr colspan="2" v-if="dbInbound.hasLink()"> </table>
<td v-if="inbound.tls"> </td>
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> </tr>
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> <tr colspan="2" v-if="dbInbound.hasLink()">
</td> <td v-if="inbound.tls">
<td v-else-if="inbound.xtls"> tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> </td>
</td> <td v-else-if="inbound.xtls">
<td v-else-if="inbound.reality"> xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> </td>
</td> <td v-else-if="inbound.reality">
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag> reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else>
tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>
</tr> </tr>
</table> </table>
@@ -85,7 +89,7 @@
<th>{{ i18n "pages.inbounds.expireDate" }}</th> <th>{{ i18n "pages.inbounds.expireDate" }}</th>
<tr> <tr>
<td> <td>
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)"> <a-tag v-if="infoModal.clientStats" color="green">
[[ sizeFormat(infoModal.clientStats['up']) ]] / [[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]] [[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]]) ([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
@@ -97,7 +101,7 @@
</td> </td>
<td> <td>
<template v-if="infoModal.clientSettings.expiryTime > 0"> <template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'"> <a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]] [[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag> </a-tag>
</template> </template>
@@ -110,6 +114,7 @@
<tr v-if="infoModal.clientSettings.subId"> <tr v-if="infoModal.clientSettings.subId">
<td>Subscription link</td> <td>Subscription link</td>
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td> <td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
<td><a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', subBase + infoModal.clientSettings.subId)"></a-icon></td>
</tr> </tr>
<tr v-if="infoModal.clientSettings.tgId"> <tr v-if="infoModal.clientSettings.tgId">
<td>Telegram ID</td> <td>Telegram ID</td>
@@ -124,7 +129,8 @@
<th>{{ i18n "encryption" }}</th> <th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th> <th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th> <th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr> </tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -136,7 +142,8 @@
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th> <th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th> <th>FollowRedirect</th>
</tr><tr> </tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -149,15 +156,18 @@
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th> <th>IP</th>
</tr><tr> </tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td> <td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'"> </tr>
<tr v-if="inbound.settings.auth == 'password'">
<td> </td> <td> </td>
<td>{{ i18n "username" }}</td> <td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts"> </tr>
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td> <td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td> <td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -169,7 +179,8 @@
<th> </th> <th> </th>
<th>{{ i18n "username" }}</th> <th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th> <th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts"> </tr>
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td> <td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td> <td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td> <td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -180,10 +191,11 @@
<div v-if="dbInbound.hasLink()"> <div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p> <p>[[ infoModal.link ]]</p>
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button> <button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
</div> </div>
</a-modal> </a-modal>
<script> <script>
const infoModal = { const infoModal = {
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
@@ -207,14 +219,6 @@
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.visible = true; this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-url-link', {
text: () => this.link,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
});
}, },
close() { close() {
infoModal.visible = false; infoModal.visible = false;
@@ -233,42 +237,38 @@
return this.infoModal.inbound; return this.infoModal.inbound;
}, },
get isActive() { get isActive() {
if(infoModal.clientStats){ if (infoModal.clientStats) {
return infoModal.clientStats.enable; return infoModal.clientStats.enable;
} }
return infoModal.dbInbound.isEnable; return infoModal.dbInbound.isEnable;
}, },
get isEnable() { get isEnable() {
if(infoModal.clientSettings){ if (infoModal.clientSettings) {
return infoModal.clientSettings.enable; return infoModal.clientSettings.enable;
} }
return infoModal.dbInbound.isEnable; return infoModal.dbInbound.isEnable;
}, },
get subBase() { get subBase() {
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + basePath + "sub/"; return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "") + basePath + "sub/";
}, },
get tgBase() { get tgBase() {
return "https://t.me/" return "https://t.me/"
}, },
}, },
methods: { methods: {
copyTextToClipboard(elmentId,content) { copyToClipboard(elmentId, content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, { this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content, text: () => content,
}); });
this.infoModal.clipboard.on('success', () => { this.infoModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}') app.$message.success('{{ i18n "copied" }}')
this.infoModal.clipboard.destroy(); this.infoModal.clipboard.destroy();
}); });
}, },
statsColor(stats) { statsColor(stats) {
if(!stats) return 'blue' return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red'
} }
}, },
}); });
</script> </script>

View File

@@ -1,7 +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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
@@ -19,7 +19,7 @@
ok() { ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound); ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
}, },
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) { show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
if (inbound) { if (inbound) {
@@ -44,7 +44,7 @@
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
@@ -87,7 +87,7 @@
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;
}, },
set delayedExpireDays(days){ set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days; this.client.expiryTime = -86400000 * days;
}, },
}, },
@@ -100,15 +100,15 @@
this.inModal.inbound.reality = false; this.inModal.inbound.reality = false;
} }
}, },
setDefaultCertData(){ setDefaultCertData(index) {
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey; inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
}, },
setDefaultCertXtls(){ setDefaultCertXtls(index) {
inModal.inbound.stream.xtls.certs[0].certFile = app.defaultCert; inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[0].keyFile = app.defaultKey; inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
}, },
async getNewX25519Cert(){ async getNewX25519Cert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert'); const msg = await HttpUtil.post('/server/getNewX25519Cert');
inModal.loading(false); inModal.loading(false);
@@ -117,15 +117,6 @@
} }
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey; inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey; inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
} }
}, },
}); });

View File

@@ -12,10 +12,11 @@
margin-top: 10px; margin-top: 10px;
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear> <transition name="list" appear>
@@ -24,7 +25,7 @@
</a-tag> </a-tag>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
<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" }}:
@@ -41,19 +42,19 @@
<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="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<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>
@@ -64,14 +65,14 @@
</a-card> </a-card>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<div slot="title"> <div slot="title">
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button> <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</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">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme"> <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>
{{ i18n "pages.inbounds.export" }} {{ i18n "pages.inbounds.export" }}
@@ -96,7 +97,7 @@
style="width: 65px;" style="width: 65px;"
v-if="isRefreshEnabled" v-if="isRefreshEnabled"
@change="changeRefreshInterval" @change="changeRefreshInterval"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :dropdown-class-name="themeSwitcher.darkCardClass">
<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>
@@ -104,7 +105,17 @@
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input> <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
</a-radio-group>
<a-switch v-model="enableFilter"
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
@change="toggleFilter">
</a-switch>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }" :loading="spinning" :scroll="{ x: 1300 }"
@@ -115,7 +126,7 @@
<a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon> <a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a> <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme"> <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>
{{ i18n "edit" }} {{ i18n "edit" }}
@@ -174,19 +185,19 @@
<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="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<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>
@@ -244,6 +255,7 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
<script> <script>
const columns = [{ const columns = [{
@@ -316,10 +328,13 @@
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
spinning: false, spinning: false,
inbounds: [], inbounds: [],
dbInbounds: [], dbInbounds: [],
searchKey: '', searchKey: '',
enableFilter: false,
filterBy: '',
searchedInbounds: [], searchedInbounds: [],
expireDiff: 0, expireDiff: 0,
trafficDiff: 0, trafficDiff: 0,
@@ -328,23 +343,25 @@
clientCount: {}, clientCount: {},
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
}, },
methods: { methods: {
loading(spinning=true) { loading(spinning = true) {
this.spinning = spinning; this.spinning = spinning;
}, },
async getDBInbounds() { async getDBInbounds() {
this.refreshing = true; this.refreshing = true;
const msg = await HttpUtil.post('/xui/inbound/list'); const msg = await HttpUtil.post('/panel/inbound/list');
if (!msg.success) { if (!msg.success) {
return; return;
} }
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
this.refreshing = false; setTimeout(() => {
this.refreshing = false;
}, 500);
}, },
async getDefaultSettings() { async getDefaultSettings() {
const msg = await HttpUtil.post('/xui/setting/defaultSettings'); const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (!msg.success) { if (!msg.success) {
return; return;
} }
@@ -361,29 +378,33 @@
to_inbound = dbInbound.toInbound() to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound); this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound); this.dbInbounds.push(dbInbound);
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){ if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound); this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
} }
} }
this.searchInbounds(this.searchKey); if(this.enableFilter){
this.filterInbounds();
} else {
this.searchInbounds(this.searchKey);
}
}, },
getClientCounts(dbInbound,inbound){ getClientCounts(dbInbound, inbound) {
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = []; let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
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()
if(clients){ if (clients) {
clientCount = clients.length; clientCount = clients.length;
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);
}); });
clientStats.forEach(client => { clientStats.forEach(client => {
if(!client.enable) { if (!client.enable) {
depleted.push(client.email); depleted.push(client.email);
} else { } else {
if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) || if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
(client.total > 0 && (client.total-(client.up+client.down) < this.trafficDiff ))) expiring.push(client.email); (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
} }
}); });
} else { } else {
@@ -409,10 +430,10 @@
if (ObjectUtil.deepSearch(inbound, key)) { if (ObjectUtil.deepSearch(inbound, key)) {
const newInbound = new DBInbound(inbound); const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings); const inboundSettings = JSON.parse(inbound.settings);
if (inboundSettings.hasOwnProperty('clients')){ if (inboundSettings.hasOwnProperty('clients')) {
const searchedSettings = { "clients": [] }; const searchedSettings = { "clients": [] };
inboundSettings.clients.forEach(client => { inboundSettings.clients.forEach(client => {
if (ObjectUtil.deepSearch(client, key)){ if (ObjectUtil.deepSearch(client, key)) {
searchedSettings.clients.push(client); searchedSettings.clients.push(client);
} }
}); });
@@ -423,7 +444,39 @@
}); });
} }
}, },
generalActions(action){ filterInbounds() {
if (ObjectUtil.isEmpty(this.filterBy)) {
this.searchedInbounds = this.dbInbounds.slice();
} else {
this.searchedInbounds.splice(0, this.searchedInbounds.length);
this.dbInbounds.forEach(inbound => {
const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings);
if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
const list = this.clientCount[inbound.id][this.filterBy];
if (list.length > 0) {
const filteredSettings = { "clients": [] };
inboundSettings.clients.forEach(client => {
if (list.includes(client.email)) {
filteredSettings.clients.push(client);
}
});
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
this.searchedInbounds.push(newInbound);
}
}
});
}
},
toggleFilter(){
if(this.enableFilter) {
this.searchKey = '';
} else {
this.filterBy = '';
this.searchedInbounds = this.dbInbounds.slice();
}
},
generalActions(action) {
switch (action.key) { switch (action.key) {
case "export": case "export":
this.exportAllLinks(); this.exportAllLinks();
@@ -476,7 +529,7 @@
break; break;
} }
}, },
openCloneInbound(dbInbound) { openCloneInbound(dbInbound) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"', title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}', content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
@@ -491,7 +544,6 @@
}); });
}, },
async cloneInbound(baseInbound, dbInbound) { async cloneInbound(baseInbound, dbInbound) {
const inbound = new Inbound();
const data = { const data = {
up: dbInbound.up, up: dbInbound.up,
down: dbInbound.down, down: dbInbound.down,
@@ -500,14 +552,14 @@
enable: dbInbound.enable, enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime, expiryTime: dbInbound.expiryTime,
listen: inbound.listen, listen: '',
port: inbound.port, port: RandomUtil.randomIntRange(10000, 60000),
protocol: baseInbound.protocol, protocol: baseInbound.protocol,
settings: inbound.settings.toString(), settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}', sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
}; };
await this.submit('/xui/inbound/add', data, inModal); await this.submit('/panel/inbound/add', data, inModal);
}, },
openAddInbound() { openAddInbound() {
inModal.show({ inModal.show({
@@ -556,7 +608,7 @@
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/xui/inbound/add', data, inModal); await this.submit('/panel/inbound/add', data, inModal);
}, },
async updateInbound(inbound, dbInbound) { async updateInbound(inbound, dbInbound) {
const data = { const data = {
@@ -575,7 +627,7 @@
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
}, },
openAddClient(dbInboundId) { openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -621,30 +673,30 @@
isEdit: true isEdit: true
}); });
}, },
findIndexOfClient(clients,client) { findIndexOfClient(clients, client) {
firstKey = Object.keys(client)[0]; firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]); return clients.findIndex(c => c[firstKey] === client[firstKey]);
}, },
async addClient(clients, dbInboundId) { async addClient(clients, dbInboundId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + clients.toString() +']}', settings: '{"clients": [' + clients.toString() + ']}',
}; };
await this.submit(`/xui/inbound/addClient`, data); await this.submit(`/panel/inbound/addClient`, data);
}, },
async updateClient(client, dbInboundId, clientId) { async updateClient(client, dbInboundId, clientId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}', settings: '{"clients": [' + client.toString() + ']}',
}; };
await this.submit(`/xui/inbound/updateClient/${clientId}`, data); await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
}, },
resetTraffic(dbInboundId) { resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => { onOk: () => {
@@ -659,26 +711,26 @@
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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId), onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId,client) { delClient(dbInboundId, client) {
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({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`), onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
}); });
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
@@ -687,22 +739,23 @@
} }
}, },
getClientId(protocol, client) { getClientId(protocol, client) {
switch(protocol){ switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email; case Protocols.SHADOWSOCKS: return client.email;
default: return client.id; default: return client.id;
} }
}, },
showQrcode(dbInbound, clientIndex) { showQrcode(dbInbound, clientIndex) {
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
const link = dbInbound.genLink(clientIndex); const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound); qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
}, },
showInfo(dbInbound, index) { showInfo(dbInbound, index) {
infoModal.show(dbInbound, index); infoModal.show(dbInbound, index);
}, },
switchEnable(dbInboundId) { switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound); this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
}, },
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client) {
this.loading() this.loading()
@@ -711,8 +764,8 @@
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client); index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable; clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol,clients[index]); clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index],dbInboundId, clientId); await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false); this.loading(false);
}, },
async submit(url, data) { async submit(url, data) {
@@ -722,100 +775,100 @@
} }
}, },
getInboundClients(dbInbound) { getInboundClients(dbInbound) {
if(dbInbound.protocol == Protocols.VLESS) { if (dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses; return dbInbound.toInbound().settings.vlesses;
} else if(dbInbound.protocol == Protocols.VMESS) { } else if (dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses; return dbInbound.toInbound().settings.vmesses;
} else if(dbInbound.protocol == Protocols.TROJAN) { } else if (dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans; return dbInbound.toInbound().settings.trojans;
} else if(dbInbound.protocol == Protocols.SHADOWSOCKS) { } else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses; return dbInbound.toInbound().settings.shadowsockses;
} }
}, },
resetClientTraffic(client,dbInboundId) { resetClientTraffic(client, dbInboundId) {
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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email), onOk: () => this.submit('/panel/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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllTraffics'), onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
}); });
}, },
resetAllClientTraffics(dbInboundId) { resetAllClientTraffics(dbInboundId) {
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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId), onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
}) })
}, },
delDepletedClients(dbInboundId) { delDepletedClients(dbInboundId) {
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: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId), onOk: () => this.submit('/panel/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
}, },
isTrafficExhausted(dbInbound, email) { statsColor(dbInbound, email) {
if(email.length == 0) return false if(email.length == 0) return 'blue';
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
}, },
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
}, },
isRemovable(dbInbound_id){ isRemovable(dbInbound_id) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1 return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
}, },
inboundLinks(dbInboundId) { inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark); txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
}, },
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');
}, },
async startDataRefreshLoop() { async startDataRefreshLoop() {
while (this.isRefreshEnabled) { while (this.isRefreshEnabled) {
try { try {
await this.getDBInbounds(); await this.getDBInbounds();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
await PromiseUtil.sleep(this.refreshInterval); await PromiseUtil.sleep(this.refreshInterval);
} }
}, },
toggleRefresh() { toggleRefresh() {
@@ -824,11 +877,11 @@
this.startDataRefreshLoop(); this.startDataRefreshLoop();
} }
}, },
changeRefreshInterval(){ changeRefreshInterval() {
localStorage.setItem("refreshInterval", this.refreshInterval); localStorage.setItem("refreshInterval", this.refreshInterval);
}, },
async manualRefresh(){ async manualRefresh() {
if(!this.refreshing){ if (!this.refreshing) {
this.spinning = true; this.spinning = true;
await this.getDBInbounds(); await this.getDBInbounds();
this.spinning = false; this.spinning = false;
@@ -876,6 +929,7 @@
} }
}, },
}); });
</script> </script>
{{template "inboundModal"}} {{template "inboundModal"}}
@@ -885,5 +939,6 @@
{{template "inboundInfoModal"}} {{template "inboundInfoModal"}}
{{template "clientsModal"}} {{template "clientsModal"}}
{{template "clientsBulkModal"}} {{template "clientsBulkModal"}}
</body> </body>
</html> </html>

View File

@@ -13,32 +13,33 @@
} }
.ant-card-dark h2 { .ant-card-dark h2 {
color: hsla(0,0%,100%,.65); color: hsla(0, 0%, 100%, .65);
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<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>
<a-row> <a-row>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU</div> <div>CPU</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="siderDrawer.isDarkTheme ? darkClass : ''" :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) ]]
@@ -51,7 +52,7 @@
<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="siderDrawer.isDarkTheme ? darkClass : ''" :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) ]]
@@ -60,7 +61,7 @@
<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="siderDrawer.isDarkTheme ? darkClass : ''" :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) ]]
@@ -75,14 +76,14 @@
<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="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a> 3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag> Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a> Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.operationHours" }}: {{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag> <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip> <a-tooltip>
@@ -94,7 +95,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="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ 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">
@@ -109,7 +110,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="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}: {{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
@@ -117,12 +118,12 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ 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="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]] TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -133,7 +134,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="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<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>
@@ -159,7 +160,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="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<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>
@@ -191,7 +192,7 @@
<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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
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>
@@ -205,7 +206,7 @@
<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="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
width="800px" width="800px"
footer=""> footer="">
<a-form layout="inline"> <a-form layout="inline">
@@ -213,7 +214,7 @@
<a-select v-model="logModal.rows" <a-select v-model="logModal.rows"
style="width: 80px" style="width: 80px"
@change="openLogs(logModal.rows)" @change="openLogs(logModal.rows)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :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>
@@ -235,13 +236,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="siderDrawer.isDarkTheme ? darkClass : ''" :closable="true" :class="themeSwitcher.darkCardClass"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()"> @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;"> <p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon> <a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]] [[ backupModal.description ]]
</p> </p>
<a-space direction="horizontal" 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 ]]
</a-button> </a-button>
@@ -253,6 +254,7 @@
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "textModal"}} {{template "textModal"}}
<script> <script>
@@ -295,13 +297,13 @@
this.disk = new CurTotal(0, 0); this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0]; this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0); this.mem = new CurTotal(0, 0);
this.netIO = {up: 0, down: 0}; this.netIO = { up: 0, down: 0 };
this.netTraffic = {sent: 0, recv: 0}; this.netTraffic = { sent: 0, recv: 0 };
this.swap = new CurTotal(0, 0); this.swap = new CurTotal(0, 0);
this.tcpCount = 0; this.tcpCount = 0;
this.udpCount = 0; this.udpCount = 0;
this.uptime = 0; this.uptime = 0;
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""}; this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" };
if (data == null) { if (data == null) {
return; return;
@@ -387,6 +389,7 @@
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
@@ -422,7 +425,7 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
@@ -448,9 +451,9 @@
return; return;
} }
}, },
async openLogs(rows){ async openLogs(rows) {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/logs/'+rows); const msg = await HttpUtil.post('server/logs/' + rows);
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
@@ -498,7 +501,7 @@
return; return;
} }
this.loading(true); this.loading(true);
const restartMsg = await HttpUtil.post("/xui/setting/restartPanel"); const restartMsg = await HttpUtil.post("/panel/setting/restartPanel");
this.loading(false); this.loading(false);
if (restartMsg.success) { if (restartMsg.success) {
this.loading(true); this.loading(true);

File diff suppressed because it is too large Load Diff

View File

@@ -673,6 +673,21 @@ func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
} }
func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, nil, err
}
if len(traffics) > 0 {
inbound, err = s.GetInbound(traffics[0].InboundId)
return traffics[0], inbound, err
}
return nil, nil, nil
}
func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB() db := database.GetDB()
var traffics []*xray.ClientTraffic var traffics []*xray.ClientTraffic
@@ -688,6 +703,85 @@ func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.Cl
return nil, nil, nil return nil, nil, nil
} }
func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {
traffic, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return nil, nil, err
}
if inbound == nil {
return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return nil, nil, err
}
for _, client := range clients {
if client.Email == clientEmail {
return traffic, &client, nil
}
}
return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
}
func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) error {
traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Traffic ID:", trafficId)
}
clientEmail := traffic.Email
oldClients, err := s.getClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["tgId"] = tgId
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) { func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail) _, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil { if err != nil {

View File

@@ -179,7 +179,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} }
func (s *ServerService) GetXrayVersions() ([]string, error) { func (s *ServerService) GetXrayVersions() ([]string, error) {
url := "https://api.github.com/repos/mhsanaei/Xray-core/releases" url := "https://api.github.com/repos/MHSanaei/Xray-core/releases"
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -246,7 +246,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
} }
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/mhsanaei/Xray-core/releases/download/%s/%s", version, fileName) url := fmt.Sprintf("https://github.com/MHSanaei/Xray-core/releases/download/%s/%s", version, fileName)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -11,7 +11,6 @@ import (
"x-ui/xray" "x-ui/xray"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"gorm.io/gorm"
) )
type SubService struct { type SubService struct {
@@ -19,15 +18,15 @@ type SubService struct {
inboundService InboundService inboundService InboundService
} }
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) { func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
s.address = host s.address = host
var result []string var result []string
var header string var headers []string
var traffic xray.ClientTraffic var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId) inbounds, err := s.getInboundsBySubId(subId)
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
for _, inbound := range inbounds { for _, inbound := range inbounds {
clients, err := s.inboundService.getClients(inbound) clients, err := s.inboundService.getClients(inbound)
@@ -38,7 +37,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
continue continue
} }
for _, client := range clients { for _, client := range clients {
if client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
@@ -66,15 +65,17 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
} }
} }
} }
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
return result, header, nil headers = append(headers, "12")
headers = append(headers, subId)
return result, headers, nil
} }
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil {
return nil, err return nil, err
} }
return inbounds, nil return inbounds, nil
@@ -326,6 +327,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["fp"] = fp params["fp"] = fp
} }
} }
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok { if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 { if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname address = sname
@@ -507,6 +513,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["fp"] = fp params["fp"] = fp
} }
} }
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok { if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 { if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname address = sname

View File

@@ -13,10 +13,13 @@ import (
"x-ui/util/common" "x-ui/util/common"
"x-ui/xray" "x-ui/xray"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil"
) )
var bot *tgbotapi.BotAPI var bot *telego.Bot
var botHandler *th.BotHandler
var adminIds []int64 var adminIds []int64
var isRunning bool var isRunning bool
@@ -61,12 +64,11 @@ func (t *Tgbot) Start() error {
adminIds = append(adminIds, int64(id)) adminIds = append(adminIds, int64(id))
} }
bot, err = tgbotapi.NewBotAPI(tgBottoken) bot, err = telego.NewBot(tgBottoken)
if err != nil { if err != nil {
fmt.Println("Get tgbot's api error:", err) fmt.Println("Get tgbot's api error:", err)
return err return err
} }
bot.Debug = false
// listen for TG bot income messages // listen for TG bot income messages
if !isRunning { if !isRunning {
@@ -83,38 +85,61 @@ func (t *Tgbot) IsRunnging() bool {
} }
func (t *Tgbot) Stop() { func (t *Tgbot) Stop() {
bot.StopReceivingUpdates() botHandler.Stop()
bot.StopLongPolling()
logger.Info("Stop Telegram receiver ...") logger.Info("Stop Telegram receiver ...")
isRunning = false isRunning = false
adminIds = nil adminIds = nil
} }
func (t *Tgbot) OnReceive() { func (t *Tgbot) OnReceive() {
u := tgbotapi.NewUpdate(0) params := telego.GetUpdatesParams{
u.Timeout = 10 Timeout: 10,
}
updates := bot.GetUpdatesChan(u) updates, _ := bot.UpdatesViaLongPolling(&params)
for update := range updates { botHandler, _ = th.NewBotHandler(bot, updates)
tgId := update.FromChat().ID
chatId := update.FromChat().ChatConfig().ChatID botHandler.HandleMessage(func(bot *telego.Bot, message telego.Message) {
isAdmin := checkAdmin(tgId) t.SendMsgToTgbot(message.Chat.ID, "Custom Keyboard Closed!", tu.ReplyKeyboardRemove())
if update.Message == nil { }, th.TextEqual("❌ Close Keyboard"))
if update.CallbackQuery != nil {
t.asnwerCallback(update.CallbackQuery, isAdmin) botHandler.HandleMessage(func(bot *telego.Bot, message telego.Message) {
} t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
} else { }, th.AnyCommand())
if update.Message.IsCommand() {
t.answerCommand(update.Message, chatId, isAdmin) botHandler.HandleCallbackQuery(func(bot *telego.Bot, query telego.CallbackQuery) {
t.asnwerCallback(&query, checkAdmin(query.From.ID))
}, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(bot *telego.Bot, message telego.Message) {
if message.UserShared != nil {
if checkAdmin(message.From.ID) {
err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10))
var output string
if err != nil {
output = "❌ Error in user selection!"
} else {
output = "✅ Telegram User saved."
}
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())
} else {
t.SendMsgToTgbot(message.Chat.ID, "No result!", tu.ReplyKeyboardRemove())
} }
} }
} }, th.AnyMessage())
botHandler.Start()
} }
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) { func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
msg := "" msg := ""
command, commandArgs := tu.ParseCommand(message.Text)
// Extract the command from the Message. // Extract the command from the Message.
switch message.Command() { switch command {
case "help": case "help":
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:" msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
case "start": case "start":
@@ -127,18 +152,18 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
case "status": case "status":
msg = "bot is ok ✅" msg = "bot is ok ✅"
case "usage": case "usage":
if len(message.CommandArguments()) > 1 { if len(commandArgs) > 0 {
if isAdmin { if isAdmin {
t.searchClient(chatId, message.CommandArguments()) t.searchClient(chatId, commandArgs[0])
} else { } else {
t.searchForClient(chatId, message.CommandArguments()) t.searchForClient(chatId, commandArgs[0])
} }
} else { } else {
msg = "❗Please provide a text for search!" msg = "❗Please provide a text for search!"
} }
case "inbound": case "inbound":
if isAdmin { if isAdmin && len(commandArgs) > 0 {
t.searchInbound(chatId, message.CommandArguments()) t.searchInbound(chatId, commandArgs[0])
} else { } else {
msg = "❗ Unknown command" msg = "❗ Unknown command"
} }
@@ -148,7 +173,9 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
t.SendAnswer(chatId, msg, isAdmin) t.SendAnswer(chatId, msg, isAdmin)
} }
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) { func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
chatId := callbackQuery.Message.Chat.ID
if isAdmin { if isAdmin {
dataArray := strings.Split(callbackQuery.Data, " ") dataArray := strings.Split(callbackQuery.Data, " ")
@@ -156,62 +183,68 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
email := dataArray[1] email := dataArray[1]
switch dataArray[0] { switch dataArray[0] {
case "client_refresh": case "client_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client Refreshed successfully.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client refreshed successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "client_cancel": case "client_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "ips_refresh": case "ips_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs Refreshed successfully.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs refreshed successfully.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
case "ips_cancel": case "ips_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
case "tgid_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client's Telegram User refreshed successfully.", email))
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
case "tgid_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
case "reset_traffic": case "reset_traffic":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup( inlineKeyboard := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel Reset", "client_cancel "+email), tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData("client_cancel "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Confirm Reset Traffic?", "reset_traffic_c "+email), tu.InlineKeyboardButton("✅ Confirm Reset Traffic?").WithCallbackData("reset_traffic_c "+email),
), ),
) )
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_traffic_c": case "reset_traffic_c":
err := t.inboundService.ResetClientTrafficByEmail(email) err := t.inboundService.ResetClientTrafficByEmail(email)
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Traffic reset successfully.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Traffic reset successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
} }
case "reset_exp": case "reset_exp":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup( var inlineKeyboard = tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel Reset", "client_cancel "+email), tu.InlineKeyboardButton("❌ Cancel Reset").WithCallbackData("client_cancel "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("♾ Unlimited", "reset_exp_c "+email+" 0"), tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData("reset_exp_c "+email+" 0"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("1 Month", "reset_exp_c "+email+" 30"), tu.InlineKeyboardButton("1 Month").WithCallbackData("reset_exp_c "+email+" 30"),
tgbotapi.NewInlineKeyboardButtonData("2 Months", "reset_exp_c "+email+" 60"), tu.InlineKeyboardButton("2 Months").WithCallbackData("reset_exp_c "+email+" 60"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("3 Months", "reset_exp_c "+email+" 90"), tu.InlineKeyboardButton("3 Months").WithCallbackData("reset_exp_c "+email+" 90"),
tgbotapi.NewInlineKeyboardButtonData("6 Months", "reset_exp_c "+email+" 180"), tu.InlineKeyboardButton("6 Months").WithCallbackData("reset_exp_c "+email+" 180"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("9 Months", "reset_exp_c "+email+" 270"), tu.InlineKeyboardButton("9 Months").WithCallbackData("reset_exp_c "+email+" 270"),
tgbotapi.NewInlineKeyboardButtonData("12 Months", "reset_exp_c "+email+" 360"), tu.InlineKeyboardButton("12 Months").WithCallbackData("reset_exp_c "+email+" 360"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("10 Days", "reset_exp_c "+email+" 10"), tu.InlineKeyboardButton("10 Days").WithCallbackData("reset_exp_c "+email+" 10"),
tgbotapi.NewInlineKeyboardButtonData("20 Days", "reset_exp_c "+email+" 20"), tu.InlineKeyboardButton("20 Days").WithCallbackData("reset_exp_c "+email+" 20"),
), ),
) )
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_exp_c": case "reset_exp_c":
if len(dataArray) == 3 { if len(dataArray) == 3 {
days, err := strconv.Atoi(dataArray[2]) days, err := strconv.Atoi(dataArray[2])
@@ -224,41 +257,41 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Expire days reset successfully.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Expire days reset successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
return return
} }
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "ip_limit": case "ip_limit":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup( inlineKeyboard := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel IP Limit", "client_cancel "+email), tu.InlineKeyboardButton("❌ Cancel IP Limit").WithCallbackData("client_cancel "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("♾ Unlimited", "ip_limit_c "+email+" 0"), tu.InlineKeyboardButton("♾ Unlimited").WithCallbackData("ip_limit_c "+email+" 0"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("1", "ip_limit_c "+email+" 1"), tu.InlineKeyboardButton("1").WithCallbackData("ip_limit_c "+email+" 1"),
tgbotapi.NewInlineKeyboardButtonData("2", "ip_limit_c "+email+" 2"), tu.InlineKeyboardButton("2").WithCallbackData("ip_limit_c "+email+" 2"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("3", "ip_limit_c "+email+" 3"), tu.InlineKeyboardButton("3").WithCallbackData("ip_limit_c "+email+" 3"),
tgbotapi.NewInlineKeyboardButtonData("4", "ip_limit_c "+email+" 4"), tu.InlineKeyboardButton("4").WithCallbackData("ip_limit_c "+email+" 4"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("5", "ip_limit_c "+email+" 5"), tu.InlineKeyboardButton("5").WithCallbackData("ip_limit_c "+email+" 5"),
tgbotapi.NewInlineKeyboardButtonData("6", "ip_limit_c "+email+" 6"), tu.InlineKeyboardButton("6").WithCallbackData("ip_limit_c "+email+" 6"),
tgbotapi.NewInlineKeyboardButtonData("7", "ip_limit_c "+email+" 7"), tu.InlineKeyboardButton("7").WithCallbackData("ip_limit_c "+email+" 7"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("8", "ip_limit_c "+email+" 8"), tu.InlineKeyboardButton("8").WithCallbackData("ip_limit_c "+email+" 8"),
tgbotapi.NewInlineKeyboardButtonData("9", "ip_limit_c "+email+" 9"), tu.InlineKeyboardButton("9").WithCallbackData("ip_limit_c "+email+" 9"),
tgbotapi.NewInlineKeyboardButtonData("10", "ip_limit_c "+email+" 10"), tu.InlineKeyboardButton("10").WithCallbackData("ip_limit_c "+email+" 10"),
), ),
) )
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "ip_limit_c": case "ip_limit_c":
if len(dataArray) == 3 { if len(dataArray) == 3 {
count, err := strconv.Atoi(dataArray[2]) count, err := strconv.Atoi(dataArray[2])
@@ -267,34 +300,60 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IP limit %d saved successfully.", email, count)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IP limit %d saved successfully.", email, count))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
return return
} }
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "clear_ips": case "clear_ips":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup( inlineKeyboard := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel", "ips_cancel "+email), tu.InlineKeyboardButton("❌ Cancel").WithCallbackData("ips_cancel "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Confirm Clear IPs?", "clear_ips_c "+email), tu.InlineKeyboardButton("✅ Confirm Clear IPs?").WithCallbackData("clear_ips_c "+email),
), ),
) )
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "clear_ips_c": case "clear_ips_c":
err := t.inboundService.ClearClientIps(email) err := t.inboundService.ClearClientIps(email)
if err == nil { if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs cleared successfully.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs cleared successfully.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
} }
case "ip_log": case "ip_log":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get IP Log.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get IP Log.", email))
t.searchClientIps(callbackQuery.From.ID, email) t.searchClientIps(chatId, email)
case "tg_user":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get Telegram User Info.", email))
t.clientTelegramUserInfo(chatId, email)
case "tgid_remove":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton("❌ Cancel").WithCallbackData("tgid_cancel "+email),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton("✅ Confirm Remove Telegram User?").WithCallbackData("tgid_remove_c "+email),
),
)
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "tgid_remove_c":
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil || traffic == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
return
}
err = t.inboundService.SetClientTelegramUserID(traffic.Id, "")
if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Telegram User removed successfully.", email))
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
case "toggle_enable": case "toggle_enable":
enabled, err := t.inboundService.ToggleClientEnableByEmail(email) enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
if err == nil { if err == nil {
@@ -304,7 +363,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Disabled successfully.", email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Disabled successfully.", email))
} }
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.") t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
} }
@@ -315,26 +374,23 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
// Respond to the callback query, telling Telegram to show the user // Respond to the callback query, telling Telegram to show the user
// a message with the data received. // a message with the data received.
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data) t.sendCallbackAnswerTgBot(callbackQuery.ID, callbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
switch callbackQuery.Data { switch callbackQuery.Data {
case "get_usage": case "get_usage":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage()) t.SendMsgToTgbot(chatId, t.getServerUsage())
case "inbounds": case "inbounds":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages()) t.SendMsgToTgbot(chatId, t.getInboundUsages())
case "deplete_soon": case "deplete_soon":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted()) t.SendMsgToTgbot(chatId, t.getExhausted())
case "get_backup": case "get_backup":
t.sendBackup(callbackQuery.From.ID) t.sendBackup(chatId)
case "client_traffic": case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName, strconv.FormatInt(callbackQuery.From.ID, 10)) t.getClientUsage(chatId, callbackQuery.From.Username, strconv.FormatInt(callbackQuery.From.ID, 10))
case "client_commands": case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.") t.SendMsgToTgbot(chatId, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
case "commands": case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>") t.SendMsgToTgbot(chatId, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
} }
} }
@@ -348,39 +404,45 @@ func checkAdmin(tgId int64) bool {
} }
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( numericKeyboard := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"), tu.InlineKeyboardButton("Server Usage").WithCallbackData("get_usage"),
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"), tu.InlineKeyboardButton("Get DB Backup").WithCallbackData("get_backup"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"), tu.InlineKeyboardButton("Get Inbounds").WithCallbackData("inbounds"),
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"), tu.InlineKeyboardButton("Deplete soon").WithCallbackData("deplete_soon"),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"), tu.InlineKeyboardButton("Commands").WithCallbackData("commands"),
), ),
) )
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup( numericKeyboardClient := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"), tu.InlineKeyboardButton("Get Usage").WithCallbackData("client_traffic"),
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"), tu.InlineKeyboardButton("Commands").WithCallbackData("client_commands"),
), ),
) )
msgConfig := tgbotapi.NewMessage(chatId, msg) params := telego.SendMessageParams{
msgConfig.ParseMode = "HTML" ChatID: tu.ID(chatId),
if isAdmin { Text: msg,
msgConfig.ReplyMarkup = numericKeyboard ParseMode: "HTML",
} else {
msgConfig.ReplyMarkup = numericKeyboardClient
} }
_, err := bot.Send(msgConfig) if isAdmin {
params.ReplyMarkup = numericKeyboard
} else {
params.ReplyMarkup = numericKeyboardClient
}
_, err := bot.SendMessage(&params)
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
} }
} }
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, inlineKeyboard ...tgbotapi.InlineKeyboardMarkup) { func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) {
if !isRunning {
return
}
var allMessages []string var allMessages []string
limit := 2000 limit := 2000
// paging message if it is big // paging message if it is big
@@ -399,12 +461,15 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, inlineKeyboard ...tgbotap
allMessages = append(allMessages, msg) allMessages = append(allMessages, msg)
} }
for _, message := range allMessages { for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message) params := telego.SendMessageParams{
info.ParseMode = "HTML" ChatID: tu.ID(chatId),
if len(inlineKeyboard) > 0 { Text: message,
info.ReplyMarkup = inlineKeyboard[0] ParseMode: "HTML",
} }
_, err := bot.Send(info) if len(replyMarkup) > 0 {
params.ReplyMarkup = replyMarkup[0]
}
_, err := bot.SendMessage(&params)
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
} }
@@ -586,12 +651,12 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips = "No IP Record" ips = "No IP Record"
} }
output := fmt.Sprintf("📧 Email: %s\r\n🔢 IPs: \r\n%s\r\n", email, ips) output := fmt.Sprintf("📧 Email: %s\r\n🔢 IPs: \r\n%s\r\n", email, ips)
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup( inlineKeyboard := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 Refresh", "ips_refresh "+email), tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("ips_refresh "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Clear IPs", "clear_ips "+email), tu.InlineKeyboardButton("❌ Clear IPs").WithCallbackData("clear_ips "+email),
), ),
) )
if len(messageID) > 0 { if len(messageID) > 0 {
@@ -601,6 +666,52 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
} }
} }
func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
traffic, client, err := t.inboundService.GetClientByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if client == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
tdId := "None"
if len(client.TgID) > 0 {
tdId = client.TgID
}
output := fmt.Sprintf("📧 Email: %s\r\n👤 Telegram User: %s\r\n", email, tdId)
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("tgid_refresh "+email),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton("❌ Remove Telegram User").WithCallbackData("tgid_remove "+email),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
requestUser := telego.KeyboardButtonRequestUser{
RequestID: int32(traffic.Id),
UserIsBot: false,
}
keyboard := tu.Keyboard(
tu.KeyboardRow(
tu.KeyboardButton("👤 Select Telegram User").WithRequestUser(&requestUser),
),
tu.KeyboardRow(
tu.KeyboardButton("❌ Close Keyboard"),
),
).WithIsPersistent().WithResizeKeyboard()
t.SendMsgToTgbot(chatId, "👤 Select a telegram user:", keyboard)
}
}
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email) traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil { if err != nil {
@@ -631,22 +742,25 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n", output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)), traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime) total, expiryTime)
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup( inlineKeyboard := tu.InlineKeyboard(
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 Refresh", "client_refresh "+email), tu.InlineKeyboardButton("🔄 Refresh").WithCallbackData("client_refresh "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📈 Reset Traffic", "reset_traffic "+email), tu.InlineKeyboardButton("📈 Reset Traffic").WithCallbackData("reset_traffic "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📅 Reset Expire Days", "reset_exp "+email), tu.InlineKeyboardButton("📅 Reset Expire Days").WithCallbackData("reset_exp "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔢 IP Log", "ip_log "+email), tu.InlineKeyboardButton("🔢 IP Log").WithCallbackData("ip_log "+email),
tgbotapi.NewInlineKeyboardButtonData("🔢 IP Limit", "ip_limit "+email), tu.InlineKeyboardButton("🔢 IP Limit").WithCallbackData("ip_limit "+email),
), ),
tgbotapi.NewInlineKeyboardRow( tu.InlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔘 Enable / Disable", "toggle_enable "+email), tu.InlineKeyboardButton("👤 Set Telegram User").WithCallbackData("tg_user "+email),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton("🔘 Enable / Disable").WithCallbackData("toggle_enable "+email),
), ),
) )
if len(messageID) > 0 { if len(messageID) > 0 {
@@ -815,41 +929,81 @@ func (t *Tgbot) getExhausted() string {
func (t *Tgbot) sendBackup(chatId int64) { func (t *Tgbot) sendBackup(chatId int64) {
sendingTime := time.Now().Format("2006-01-02 15:04:05") sendingTime := time.Now().Format("2006-01-02 15:04:05")
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime) t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
file := tgbotapi.FilePath(config.GetDBPath()) file, err := os.Open(config.GetDBPath())
msg := tgbotapi.NewDocument(chatId, file) if err != nil {
_, err := bot.Send(msg) logger.Warning("Error in opening db file for backup: ", err)
}
document := tu.Document(
tu.ID(chatId),
tu.File(file),
)
_, err = bot.SendDocument(document)
if err != nil { if err != nil {
logger.Warning("Error in uploading backup: ", err) logger.Warning("Error in uploading backup: ", err)
} }
file = tgbotapi.FilePath(xray.GetConfigPath()) file, err = os.Open(xray.GetConfigPath())
msg = tgbotapi.NewDocument(chatId, file) if err != nil {
_, err = bot.Send(msg) logger.Warning("Error in opening config.json file for backup: ", err)
}
document = tu.Document(
tu.ID(chatId),
tu.File(file),
)
_, err = bot.SendDocument(document)
if err != nil { if err != nil {
logger.Warning("Error in uploading config.json: ", err) logger.Warning("Error in uploading config.json: ", err)
} }
} }
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) { func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
callback := tgbotapi.NewCallback(id, message) params := telego.AnswerCallbackQueryParams{
if _, err := bot.Request(callback); err != nil { CallbackQueryID: id,
Text: message,
}
if err := bot.AnswerCallbackQuery(&params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard tgbotapi.InlineKeyboardMarkup) { func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageReplyMarkup(chatId, messageID, inlineKeyboard) params := telego.EditMessageReplyMarkupParams{
if _, err := bot.Request(edit); err != nil { ChatID: tu.ID(chatId),
MessageID: messageID,
ReplyMarkup: inlineKeyboard,
}
if _, err := bot.EditMessageReplyMarkup(&params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...tgbotapi.InlineKeyboardMarkup) { func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageText(chatId, messageID, text) params := telego.EditMessageTextParams{
edit.ParseMode = "HTML" ChatID: tu.ID(chatId),
MessageID: messageID,
Text: text,
ParseMode: "HTML",
}
if len(inlineKeyboard) > 0 { if len(inlineKeyboard) > 0 {
edit.ReplyMarkup = &inlineKeyboard[0] params.ReplyMarkup = inlineKeyboard[0]
} }
if _, err := bot.Request(edit); err != nil { if _, err := bot.EditMessageText(&params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
func fromChat(u *telego.Update) *telego.Chat {
switch {
case u.Message != nil:
return &u.Message.Chat
case u.EditedMessage != nil:
return &u.EditedMessage.Chat
case u.ChannelPost != nil:
return &u.ChannelPost.Chat
case u.EditedChannelPost != nil:
return &u.EditedChannelPost.Chat
case u.CallbackQuery != nil:
return &u.CallbackQuery.Message.Chat
default:
return nil
}
}

View File

@@ -11,7 +11,7 @@
"enable" = "Enable" "enable" = "Enable"
"protocol" = "Protocol" "protocol" = "Protocol"
"search" = "Search" "search" = "Search"
"filter" = "Filter"
"loading" = "Loading" "loading" = "Loading"
"second" = "Second" "second" = "Second"
"minute" = "Minute" "minute" = "Minute"
@@ -148,10 +148,6 @@
"resetAllTraffic" = "Reset All Inbounds Traffic" "resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic" "resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?" "resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
"resetAllTrafficOkText" = "Confirm"
"resetAllTrafficCancelText" = "Cancel"
"IPLimit" = "IP Limit"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (enter 0 to disable IP limit)."
"resetInboundClientTraffics" = "Reset Clients Traffic" "resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all client traffic" "resetInboundClientTrafficTitle" = "Reset all client traffic"
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?" "resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
@@ -161,8 +157,10 @@
"delDepletedClients" = "Delete Depleted Clients" "delDepletedClients" = "Delete Depleted Clients"
"delDepletedClientsTitle" = "Delete depleted clients" "delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?" "delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
"Email" = "Email" "email" = "Email"
"EmailDesc" = "Please provide a unique email address." "emailDesc" = "Please provide a unique email address."
"IPLimit" = "IP Limit"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (enter 0 to disable IP limit)."
"IPLimitlog" = "IP Log" "IPLimitlog" = "IP Log"
"IPLimitlogDesc" = "IPs history log (before enabling inbound after it has been disabled by IP limit, you should clear the log)." "IPLimitlogDesc" = "IPs history log (before enabling inbound after it has been disabled by IP limit, you should clear the log)."
"IPLimitlogclear" = "Clear The Log" "IPLimitlogclear" = "Clear The Log"
@@ -170,7 +168,7 @@
"xtlsDesc" = "Xray core needs to be 1.7.5" "xtlsDesc" = "Xray core needs to be 1.7.5"
"realityDesc" = "Xray core needs to be 1.8.0 or higher." "realityDesc" = "Xray core needs to be 1.8.0 or higher."
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )" "telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
"subscriptionDesc" = "you can find your sub link on Details, also ou 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"
[pages.client] [pages.client]
"add" = "Add Client" "add" = "Add Client"
@@ -211,6 +209,7 @@
[pages.settings] [pages.settings]
"title" = "Settings" "title" = "Settings"
"save" = "Save" "save" = "Save"
"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
"restartPanel" = "Restart Panel " "restartPanel" = "Restart Panel "
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server." "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
"actions" = "Actions" "actions" = "Actions"
@@ -220,29 +219,29 @@
"xrayConfiguration" = "Xray Configuration" "xrayConfiguration" = "Xray Configuration"
"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. Restart the panel to apply changes." "panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
"panelPort" = "Panel Port" "panelPort" = "Panel Port"
"panelPortDesc" = "Restart the panel to apply changes." "panelPortDesc" = "The port used to display this panel"
"publicKeyPath" = "Panel Certificate Public Key File Path" "publicKeyPath" = "Panel Certificate Public Key File Path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes." "publicKeyPathDesc" = "Fill in an absolute path starting with."
"privateKeyPath" = "Panel Certificate Private Key File Path" "privateKeyPath" = "Panel Certificate Private Key File Path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes." "privateKeyPathDesc" = "Fill in an absolute path starting with."
"panelUrlPath" = "Panel URL Root Path" "panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with '/'. Restart the panel to apply changes." "panelUrlPathDesc" = "Must start with '/' and end with."
"oldUsername" = "Current Username" "oldUsername" = "Current Username"
"currentPassword" = "Current Password" "currentPassword" = "Current Password"
"newUsername" = "New Username" "newUsername" = "New Username"
"newPassword" = "New Password" "newPassword" = "New Password"
"telegramBotEnable" = "Enable Telegram bot" "telegramBotEnable" = "Enable Telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect." "telegramBotEnableDesc" = "Connect to the features of this panel through the Telegram bot"
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect." "telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
"telegramChatId" = "Telegram Admin Chat IDs" "telegramChatId" = "Telegram Admin Chat IDs"
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs. Restart the panel to apply changes." "telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs."
"telegramNotifyTime" = "Telegram bot notification time" "telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Use Crontab timing format. Restart the panel to apply changes." "telegramNotifyTimeDesc" = "Use Crontab timing format."
"tgNotifyBackup" = "Database Backup" "tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Include database backup file with report notification. Restart the panel to apply changes." "tgNotifyBackupDesc" = "Include database backup file with report notification."
"sessionMaxAge" = "Session maximum age" "sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The duration of a login session (unit: minute)" "sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
"expireTimeDiff" = "Expiration threshold for notification" "expireTimeDiff" = "Expiration threshold for notification"
@@ -252,7 +251,7 @@
"tgNotifyCpu" = "CPU percentage alert threshold" "tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)" "tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
"timeZone" = "Time zone" "timeZone" = "Time zone"
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone. Restart the panel to apply changes." "timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
[pages.settings.templates] [pages.settings.templates]
"title" = "Templates" "title" = "Templates"
@@ -260,55 +259,83 @@
"advancedTemplate" = "Advanced Template" "advancedTemplate" = "Advanced Template"
"completeTemplate" = "Complete Template" "completeTemplate" = "Complete Template"
"generalConfigs" = "General Configs" "generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites." "generalConfigsDesc" = "These options will provide general adjustments."
"countryConfigs" = "Country Configs" "blockConfigs" = "Blocking Configs"
"countryConfigsDesc" = "These options will prevent users from connecting to specific country domains." "blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"blockCountryConfigs" = "Block Country Configs"
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"directCountryConfigs" = "Direct Country Configs"
"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."
"warpConfigs" = "WARP Configs" "warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers." "warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"xrayConfigTemplate" = "Xray Configuration Template" "xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template. Restart the panel to apply changes." "xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template."
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol"
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy"
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
"xrayConfigTorrent" = "Ban BitTorrent Usage" "xrayConfigTorrent" = "Ban BitTorrent Usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users. Restart the panel to apply changes." "xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect" "xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges. Restart the panel to apply changes." "xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
"xrayConfigAds" = "Block Ads" "xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block ads. Restart the panel to apply changes." "xrayConfigAdsDesc" = "Change the configuration template to block ads."
"xrayConfigPorn" = "Block Porn Websites" "xrayConfigFamily" = "Block Malware and Adult Content"
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to porn websites. Restart the panel to apply changes." "xrayConfigFamilyDesc" = "DNS resolvers to block malware and adult content for family protection."
"xrayConfigSpeedtest" = "Block Speedtest Websites"
"xrayConfigSpeedtestDesc" = "Change the configuration template to avoid connecting to speedtest websites."
"xrayConfigIRIp" = "Disable connection to Iran IP ranges" "xrayConfigIRIp" = "Disable connection to Iran IP ranges"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges. Restart the panel to apply changes." "xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
"xrayConfigIRDomain" = "Disable connection to Iran domains" "xrayConfigIRDomain" = "Disable connection to Iran domains"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting with Iran domains. Restart the panel to apply changes." "xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
"xrayConfigChinaIp" = "Disable connection to China IP ranges" "xrayConfigChinaIp" = "Disable connection to China IP ranges"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting with China IP ranges. Restart the panel to apply changes." "xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
"xrayConfigChinaDomain" = "Disable connection to China domains" "xrayConfigChinaDomain" = "Disable connection to China domains"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting with China domains. Restart the panel to apply changes." "xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges" "xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting with Russia IP ranges. Restart the panel to apply changes." "xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
"xrayConfigRussiaDomain" = "Disable connection to Russia domains" "xrayConfigRussiaDomain" = "Disable connection to Russia domains"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting with Russia domains. Restart the panel to apply changes." "xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges"
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains"
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges"
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
"xrayConfigDirectChinaDomain" = "Direct connection to China domains"
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges"
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains"
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
"xrayConfigGoogleIPv4" = "Use IPv4 for Google" "xrayConfigGoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4. Restart the panel to apply changes." "xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix" "xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4. Restart the panel to apply changes." "xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
"xrayConfigGoogleWARP" = "Route Google through WARP." "xrayConfigGoogleWARP" = "Route Google through WARP."
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP. Restart the panel to apply changes." "xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP."
"xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) through WARP." "xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) through WARP."
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP. Restart the panel to apply changes." "xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP."
"xrayConfigNetflixWARP" = "Route Netflix through WARP." "xrayConfigNetflixWARP" = "Route Netflix through WARP."
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP. Restart the panel to apply changes." "xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP."
"xrayConfigSpotifyWARP" = "Route Spotify through WARP." "xrayConfigSpotifyWARP" = "Route Spotify through WARP."
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP. Restart the panel to apply changes." "xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP."
"xrayConfigIRWARP" = "Route Iran domains through WARP." "xrayConfigIRWARP" = "Route Iran domains through WARP."
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP. Restart the panel to apply changes." "xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP."
"xrayConfigInbounds" = "Configuration of Inbounds" "xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients. Restart the panel to apply changes." "xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients."
"xrayConfigOutbounds" = "Configuration of Outbounds" "xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server. Restart the panel to apply changes." "xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
"xrayConfigRoutings" = "Configuration of routing rules." "xrayConfigRoutings" = "Configuration of routing rules."
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server. Restart the panel to apply changes." "xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
"manualLists" = "Manual Lists"
"manualListsDesc" = "Please use the JSON array format."
"manualBlockedIPs" = "List of Blocked IPs"
"manualBlockedDomains" = "List of Blocked Domains"
"manualDirectIPs" = "List of Direct IPs"
"manualDirectDomains" = "List of Direct Domains"
[pages.settings.security] [pages.settings.security]
"admin" = "Admin" "admin" = "Admin"

View File

@@ -11,7 +11,7 @@
"enable" = "فعال" "enable" = "فعال"
"protocol" = "پروتکل" "protocol" = "پروتکل"
"search" = "جستجو" "search" = "جستجو"
"filter" = "فیلتر"
"loading" = "در حال بروزرسانی..." "loading" = "در حال بروزرسانی..."
"second" = "ثانیه" "second" = "ثانیه"
"minute" = "دقیقه" "minute" = "دقیقه"
@@ -77,7 +77,7 @@
"restartXray" = "شروع مجدد" "restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن" "xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید" "xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ." "xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
"operationHours" = "مدت فعالیت" "operationHours" = "مدت فعالیت"
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن" "operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
"systemLoad" = "بار روی سیستم" "systemLoad" = "بار روی سیستم"
@@ -94,7 +94,7 @@
"config" = "تنظیمات" "config" = "تنظیمات"
"backup" = "پشتیبان گیری و بازیابی" "backup" = "پشتیبان گیری و بازیابی"
"backupTitle" = "پشتیبان گیری و بازیابی دیتابیس" "backupTitle" = "پشتیبان گیری و بازیابی دیتابیس"
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید." "backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
"exportDatabase" = "دانلود دیتابیس" "exportDatabase" = "دانلود دیتابیس"
"importDatabase" = "آپلود دیتابیس" "importDatabase" = "آپلود دیتابیس"
@@ -157,10 +157,10 @@
"delDepletedClients" = "حذف کاربران منقضی" "delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی" "delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟" "delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"email" = "ایمیل"
"emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"IPLimit" = "محدودیت ای پی" "IPLimit" = "محدودیت ای پی"
"IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )" "IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
"Email" = "ایمیل"
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"IPLimitlog" = "گزارش ها" "IPLimitlog" = "گزارش ها"
"IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)" "IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
"IPLimitlogclear" = "پاک کردن گزارش ها" "IPLimitlogclear" = "پاک کردن گزارش ها"
@@ -209,6 +209,7 @@
[pages.settings] [pages.settings]
"title" = "تنظیمات" "title" = "تنظیمات"
"save" = "ذخیره" "save" = "ذخیره"
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
"restartPanel" = "ریستارت پنل" "restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید" "restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"actions" = "عملیات ها" "actions" = "عملیات ها"
@@ -218,27 +219,27 @@
"xrayConfiguration" = "تنظیمات Xray" "xrayConfiguration" = "تنظیمات Xray"
"TGBotSettings" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل" "panelPort" = "پورت پنل"
"panelPortDesc" = نل را مجدداً راه اندازی کنید تا اعمال شود" "panelPortDesc" = ورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود" "publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل" "privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود" "privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"panelUrlPath" = "آدرس روت پنل" "panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود" "panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
"oldUsername" = "نام کاربری فعلی" "oldUsername" = "نام کاربری فعلی"
"currentPassword" = "رمز عبور فعلی" "currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید" "newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید" "newPassword" = "رمز عبور جدید"
"telegramBotEnable" = "فعالسازی ربات تلگرام" "telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات این پنل متصل شوید"
"telegramToken" = "توکن تلگرام" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت" "telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای" "tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب" "sessionMaxAge" = "بیشینه زمان جلسه وب"
@@ -258,55 +259,83 @@
"advancedTemplate" = "بخش الگو پیشرفته" "advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل" "completeTemplate" = "بخش الگو کامل"
"generalConfigs" = "تنظیمات عمومی" "generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند." "generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
"countryConfigs" = "تنظیمات برای کشورها" "blockConfigs" = "مسدود سازی"
"countryConfigsDesc" = "این گزینه از اتصال کاربران به دامنه های کشوری خاص جلوگیری می کند." "blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
"ipv4Configs" = "تنظیمات برای IPv4" "ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن 4 به دامنه های هدف هدایت می شود." "ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
"warpConfigs" = "تنظیمات برای WARP" "warpConfigs" = "تنظیمات برای WARP"
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند." "warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری" "xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت" "xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی" "xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
"xrayConfigAds" = "مسدود کردن تبلیغات" "xrayConfigAds" = "مسدود کردن تبلیغات"
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
"xrayConfigPorn" = "جلوگیری از اتصال به سایت های پورن" "xrayConfigFamily" = "فعال کردن حالت خانواده"
"xrayConfigPornDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های پورن تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
"xrayConfigSpeedtest" = "جلوگیری از اتصال به سایت های تست سرعت"
"xrayConfigSpeedtestDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های تست سرعت تغییر میدهد"
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران" "xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران" "xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین" "xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین" "xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه" "xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه" "xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل" "xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس" "xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
"xrayConfigGoogleWARP" = "مسیردهی گوگل به WARP" "xrayConfigGoogleWARP" = "مسیردهی گوگل به WARP"
"xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند"
"xrayConfigOpenAIWARP" = "مسیردهی OpenAI (ChatGPT) به WARP" "xrayConfigOpenAIWARP" = "مسیردهی OpenAI (ChatGPT) به WARP"
"xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند"
"xrayConfigNetflixWARP" = "مسیردهی نتفلیکس به WARP" "xrayConfigNetflixWARP" = "مسیردهی نتفلیکس به WARP"
"xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند"
"xrayConfigSpotifyWARP" = "مسیردهی اسپاتیفای به WARP" "xrayConfigSpotifyWARP" = "مسیردهی اسپاتیفای به WARP"
"xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند"
"xrayConfigIRWARP" = "مسیردهی دامنه های ایران به WARP" "xrayConfigIRWARP" = "مسیردهی دامنه های ایران به WARP"
"xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند"
"xrayConfigInbounds" = "تنظیمات ورودی" "xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
"xrayConfigOutbounds" = "تنظیمات خروجی" "xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی" "xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
"manualLists" = "لیست های دستی"
"manualListsDesc" = "فرمت: JSON Array"
"manualBlockedIPs" = "لیست آی‌پی های مسدود شده"
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
"manualDirectIPs" = "لیست آی‌پی های مستقیم"
"manualDirectDomains" = "لیست دامنه های مستقیم"
[pages.settings.security] [pages.settings.security]
"admin" = "مدیر" "admin" = "مدیر"
@@ -320,5 +349,5 @@
"modifySettings" = "ویرایش تنظیمات" "modifySettings" = "ویرایش تنظیمات"
"getSettings" = "دریافت تنظیمات" "getSettings" = "دریافت تنظیمات"
"modifyUser" = "ویرایش کاربر" "modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ." "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ." "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "

View File

@@ -0,0 +1,353 @@
"username" = "имя пользователя"
"password" = "пароль"
"login" = "логин"
"confirm" = "подтвердить"
"cancel" = "отмена"
"close" = "закрыть"
"copy" = "копировать"
"copied" = "скопировано"
"download" = "скачать"
"remark" = "примечание"
"enable" = "включить"
"protocol" = "протокол"
"search" = "поиск"
"filter" = "Фильтр"
"loading" = "загрузка"
"second" = "секунда"
"minute" = "минута"
"hour" = "час"
"day" = "день"
"check" = "просмотр"
"indefinite" = "бессрочно"
"unlimited" = "безлимитно"
"none" = "пусто"
"qrCode" = "QR-код"
"info" = "больше информации"
"edit" = "изменить"
"delete" = "удалить"
"reset" = "обнулить"
"copySuccess" = "скопировано"
"sure" = "да"
"encryption" = "Шифрование"
"transmission" = "протокол передачи"
"host" = "хост"
"path" = "путь"
"camouflage" = "маскировка"
"status" = "статус"
"enabled" = "включено"
"disabled" = "отключено"
"depleted" = "исчерпано"
"depletingSoon" = "почти исчерпано"
"domainName" = "домен"
"additional" = "допольнительно"
"monitor" = "порт IP"
"certificate" = "сертификат"
"fail" = "неудача"
"success" = "успешно"
"getVersion" = "узнать версию"
"install" = "установка"
"clients" = "клиенты"
"usage" = "использование"
"secretToken" = "секретный токен"
[menu]
"dashboard" = "статус системы"
"inbounds" = "пользователи"
"settings" = "настройки"
"logout" = "выход"
"link" = "другое"
[pages.login]
"title" = "логин"
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
[pages.login.toasts]
"invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
"successLogin" = "успешный вход"
[pages.index]
"title" = "статус системы"
"memory" = "память"
"hard" = "жесткий диск"
"xrayStatus" = "статус Xray"
"stopXray" = "стоп"
"restartXray" = "рестарт Xray"
"xraySwitch" = "переключить версию"
"xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
"operationHours" = "Часы работы"
"operationHoursDesc" = "Аптайм системы: время системы в сети"
"systemLoad" = "Системная нагрузка"
"connectionCount" = "количество соединений"
"connectionCountDesc" = "Всего подключений по всем сетям»"
"upSpeed" = "Общая скорость upload"
"downSpeed" = "Общая скорость download"
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
"totalReceive" = "Общий объем полученных данных с момента запуска системы."
"xraySwitchVersionDialog" = "переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Установка. Не обновляйте эту страницу"
"logs" = "Логи"
"config" = "Конфиг"
"backup" = "Бекап и восстановление"
"backupTitle" = "База данных бекапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
"exportDatabase" = "Экспорт базы данных"
"importDatabase" = "Импорт базы данных"
[pages.inbounds]
"title" = "пользователи"
"totalDownUp" = "Всего входящих/исходящих"
"totalUsage" = "Всего использовано"
"inboundCount" = "Количество пользователей"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"traffic" = "Траффик"
"details" = "Подробнее"
"transportConfig" = "Перенести"
"expireDate" = "Дата окончания"
"resetTraffic" = "Обнулить траффик"
"addInbound" = "Добавить пользователя"
"generalActions" = "Общие действия"
"create" = "Создать"
"update" = "Обновить"
"modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить пользователя"
"deleteInboundContent" = "Подтвердите удаление пользователя?"
"resetTrafficContent" = "Подтвердите обнуление траффика?"
"copyLink" = "Копировать ключ"
"address" = "Адрес"
"network" = "Сеть"
"destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес"
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
"monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы никогда не истекать"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
"publicKeyPath" = "Путь к публичному ключу"
"publicKeyContent" = "Содержимое публичного ключа"
"keyPath" = "Путь к приватному ключу"
"keyContent" = "Содержимое приватного ключа"
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
"client" = "Клиент"
"export" = "Поделиться ключом"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать пользователя"
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы"
"cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Обнулить весь траффик"
"resetAllTrafficTitle" = "Обнуление всего траффика"
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"delDepletedClients" = "Удалить отключенных пользователей"
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "ограничение по IP"
"IPLimitDesc" = "Отключить ключ, если подключено больше введенного значения (введите 0, чтобы отключить ограничение IP-адресов)"
"IPLimitlog" = "IP лог"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить список)"
"IPLimitlogclear" = "Очистить список"
"setDefaultCert" = "Установить сертификат с панели"
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
"realityDesc" = "Версия Xray должна быть 1.8.0 или выше"
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)"
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
[pages.client]
"add" = "Добавить пользователя"
"edit" = "Редактировать пользователя"
"submitAdd" = "Добавить пользователя"
"submitEdit" = "Сохранить изменения"
"clientCount" = "Количество пользователей"
"bulk" = "Добавить несколько"
"method" = "Метод"
"first" = "Первый"
"last" = "Последний"
"prefix" = "Префикс"
"postfix" = "Постфикс"
"delayedStart" = "Начать со времени первого подключения"
"expireDays" = "Срок действия"
"days" = "дней"
[pages.inbounds.toasts]
"obtain" = "Получить"
[pages.inbounds.stream.general]
"requestHeader" = "Требуется заголовок"
"name" = "Имя"
"value" = "Значение"
[pages.inbounds.stream.tcp]
"requestVersion" = "Требуется версия"
"requestMethod" = "Требуется метод"
"requestPath" = "Требуется путь"
"responseVersion" = "Указать версию"
"responseStatus" = "Указать статус"
"responseStatusDescription" = "Указать примечание статуса"
"responseHeader" = "Указать заголовок"
[pages.inbounds.stream.quic]
"encryption" = "Шифрование"
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"infoDesc" = "Каждое сделанное здесь изменение необходимо сохранить. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу"
"restartPanel" = "Рестарт панели"
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
"actions" = "Действия"
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
"panelSettings" = "Настройки панели"
"securitySettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-порт панели"
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP"
"panelPort" = "Порт панели"
"panelPortDesc" = "Порт, используемый для отображения этой панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с "
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с "
"panelUrlPath" = "Корневой путь URL-адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на "
"oldUsername" = "Имя пользователя сейчас"
"currentPassword" = "Пароль сейчас"
"newUsername" = "Новое имя пользователя"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Телеграм-бота"
"telegramBotEnableDesc" = "Подключайтесь к возможностям этой панели через Telegram-бота"
"telegramToken" = "Токен Телеграм-бота"
"telegramTokenDesc" = "Получить токен необходимо у менеджера ботов Telegram @botfather"
"telegramChatId" = "Телеграм-ID админа бота"
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID"
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
"telegramNotifyTimeDesc" = "Используйте формат Crontab"
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете"
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог траффика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
"timeZone" = "Временная зона"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе"
[pages.settings.templates]
"title" = "Шаблоны"
"basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Конфигурация шаблона"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockConfigs" = "Блокировка конфигураций"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockCountryConfigs" = "Заблокировать конфигурации страны"
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны"
"directCountryConfigs" = "Прямые настройки страны"
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны"
"ipv4Configs" = "Настройки IPv4 "
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
"warpConfigs" = "Настройки WARP"
"warpConfigsDesc" = "Внимание: перед использованием этих параметров установите WARP в режиме прокси-сервера socks5 на свой сервер, следуя инструкциям на GitHub панели. WARP будет направлять трафик на веб-сайты через серверы Cloudflare"
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона"
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
"xrayConfigTorrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent"
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов"
"xrayConfigAds" = "Бокировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу"
"xrayConfigFamily" = "Блокировать вредоносное ПО и контент для взрослых"
"xrayConfigFamilyDesc" = "Преобразователи DNS для блокировки вредоносных программ и контента для взрослых для защиты семьи"
"xrayConfigSpeedtest" = "Блокировать сайты для проверки скорости"
"xrayConfigSpeedtestDesc" = "Измените шаблон конфигурации, чтобы избежать подключения к веб-сайтам для тестирования скорости"
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана"
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая"
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России"
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России"
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
"xrayConfigDirectIRDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Ирана"
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая"
"xrayConfigDirectChinaIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Китая"
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
"xrayConfigDirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая"
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
"xrayConfigDirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России"
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4"
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4"
"xrayConfigGoogleWARP" = "Маршрутизация Google через WARP"
"xrayConfigGoogleWARPDesc" = "Применить маршрутизацию Google через WARP"
"xrayConfigOpenAIWARP" = "Маршрут OpenAI (ChatGPT) через WARP"
"xrayConfigOpenAIWARPDesc" = "Применить маршрутизацию для OpenAI (ChatGPT) через WARP"
"xrayConfigNetflixWARP" = "Маршрутизация Netflix через WARP"
"xrayConfigNetflixWARPDesc" = "Применить маршрутизацию Netflix через WARP"
"xrayConfigSpotifyWARP" = "Маршрутизация Spotify через WARP"
"xrayConfigSpotifyWARPDesc" = "Применить маршрутизацию Spotify через WARP"
"xrayConfigIRWARP" = "Маршрутизация доменов Ирана через WARP"
"xrayConfigIRWARPDesc" = "Применить маршрутизацию для доменов Ирана через WARP"
"xrayConfigInbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей"
"xrayConfigOutbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
"xrayConfigRoutings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера"
"manualLists" = "ручные списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP-адресов"
"manualBlockedDomains" = "Список заблокированных доменов"
"manualDirectIPs" = "Список прямых IP-адресов"
"manualDirectDomains" = "Список прямых доменов"
[pages.settings.security]
"admin" = "Админ"
"secret" = "Секретный токен"
"loginSecurity" = "Безопасность входа"
"loginSecurityDesc" = "Включить дополнительные меры безопасности входа пользователя"
"secretToken" = "Секретный токен"
"secretTokenDesc" = "Пожалуйста, скопируйте и сохраните этот токен в безопасном месте. Этот токен необходим для входа в систему и не может быть восстановлен с помощью командного инструмента x-ui"
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
"getSettings" = "Просмотр настроек"
"modifyUser" = "Изменение пользователя "
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"

View File

@@ -11,7 +11,7 @@
"enable" = "启用" "enable" = "启用"
"protocol" = "协议" "protocol" = "协议"
"search" = "搜尋" "search" = "搜尋"
"filter" = "过滤器"
"loading" = "加载中" "loading" = "加载中"
"second" = "秒" "second" = "秒"
"minute" = "分钟" "minute" = "分钟"
@@ -157,10 +157,10 @@
"delDepletedClients" = "删除耗尽的客户端" "delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户" "delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?" "delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"email" = "电子邮件"
"emailDesc" = "电子邮件必须完全唯"
"IPLimit" = "IP限制" "IPLimit" = "IP限制"
"IPLimitDesc" = "如果超过输入的计数则禁用入站0 表示禁用限制 ip" "IPLimitDesc" = "如果超过输入的计数则禁用入站0 表示禁用限制 ip"
"Email" = "电子邮件"
"EmailDesc" = "电子邮件必须完全唯"
"IPLimitlog" = "IP日志" "IPLimitlog" = "IP日志"
"IPLimitlogDesc" = "IP 历史日志 通过IP限制禁用inbound之前需要清空日志" "IPLimitlogDesc" = "IP 历史日志 通过IP限制禁用inbound之前需要清空日志"
"IPLimitlogclear" = "清除日志" "IPLimitlogclear" = "清除日志"
@@ -209,6 +209,7 @@
[pages.settings] [pages.settings]
"title" = "设置" "title" = "设置"
"save" = "保存配置" "save" = "保存配置"
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
"restartPanel" = "重启面板" "restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息" "restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"actions" = "动作" "actions" = "动作"
@@ -218,15 +219,15 @@
"xrayConfiguration" = "xray 相关设置" "xrayConfiguration" = "xray 相关设置"
"TGBotSettings" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效" "panelListeningIPDesc" = "默认留空监听所有 IP"
"panelPort" = "面板监听端口" "panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效" "panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径" "publicKeyPath" = "面板证书公钥文件路径"
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效" "publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"privateKeyPath" = "面板证书密钥文件路径" "privateKeyPath" = "面板证书密钥文件路径"
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效" "privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"panelUrlPath" = "面板 url 根路径" "panelUrlPath" = "面板 url 根路径"
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效" "panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
"oldUsername" = "原用户名" "oldUsername" = "原用户名"
"currentPassword" = "原密码" "currentPassword" = "原密码"
"newUsername" = "新用户名" "newUsername" = "新用户名"
@@ -240,7 +241,7 @@
"telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份" "tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效" "tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
"sessionMaxAge" = "会话最大年龄" "sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)" "sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值" "expireTimeDiff" = "耗尽时间阈值"
@@ -250,63 +251,91 @@
"tgNotifyCpu" = "CPU 百分比警报阈值" "tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知" "tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZone" = "时区" "timeZone" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效" "timeZoneDesc" = "定时任务按照该时区的时间运行"
[pages.settings.templates] [pages.settings.templates]
"title" = "模板" "title" = "模板"
"basicTemplate" = "基本模板" "basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件" "advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板" "completeTemplate" = "Xray 配置的完整模板"
"generalConfigs" = "一般配置" "generalConfigs" = "通用配置"
"generalConfigsDesc" = "选项将阻止用户连接到特定协议和网站。" "generalConfigsDesc" = "这些选项将提供一般调整"
"countryConfigs" = "国家配置" "blockConfigs" = "阻塞配置"
"countryConfigsDesc" = "选项将阻止用户连接到特定国家/地区的域。" "blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
"blockCountryConfigs" = "阻止国家配置"
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
"directCountryConfigs" = "直接国家配置"
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
"ipv4Configs" = "IPv4 配置" "ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域" "ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
"warpConfigs" = "WARP 配置" "warpConfigs" = "WARP 配置"
"warpConfigsDesc" = "警告:在使用选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。" "warpConfigsDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
"xrayConfigTemplate" = "xray 配置模板" "xrayConfigTemplate" = "Xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率" "xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件重新启动面板生成效率"
"xrayConfigFreedomStrategy" = "配置自由协议的策略"
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略"
"xrayConfigRoutingStrategy" = "配置路由域策略"
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略"
"xrayConfigTorrent" = "禁止使用 bittorrent" "xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效" "xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent"
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接" "xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效" "xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
"xrayConfigAds" = "屏蔽广告" "xrayConfigAds" = "屏蔽广告"
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告,重启面板生效" "xrayConfigAdsDesc" = "修改配置模板屏蔽广告"
"xrayConfigPorn" = "禁止色情网站连接" "xrayConfigFamily" = "启用家庭友好配置"
"xrayConfigPornDesc" = "更改配置模板避免连接色情网站,重启面板生效" "xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站"
"xrayConfigSpeedtest" = "阻止测速网站"
"xrayConfigSpeedtestDesc" = "更改配置模板以避免连接到速度测试网站。 重新启动面板以应用更改。"
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接" "xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段,重启面板生效" "xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段"
"xrayConfigIRDomain" = "禁止伊朗域连接" "xrayConfigIRDomain" = "禁止伊朗域连接"
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名,重启面板生效" "xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名"
"xrayConfigChinaIp" = "禁止中国 IP 范围连接" "xrayConfigChinaIp" = "禁止中国 IP 范围连接"
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段,重启面板生效" "xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段"
"xrayConfigChinaDomain" = "禁止中国域名连接" "xrayConfigChinaDomain" = "禁止中国域名连接"
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域,重启面板生效" "xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域"
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接" "xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接"
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围,重启面板生效" "xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接" "xrayConfigRussiaDomain" = "禁止俄罗斯域连接"
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域,重启面板生效" "xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
"xrayConfigDirectIRIp" = "直接连接到伊朗 IP 范围"
"xrayConfigDirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
"xrayConfigDirectIRDomain" = "直接连接到伊朗域"
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
"xrayConfigDirectChinaIp" = "直连中国IP范围"
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
"xrayConfigDirectChinaDomain" = "直连中国域名"
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板"
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域"
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4" "xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由,重启面板生效" "xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4" "xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由,重启面板生效" "xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
"xrayConfigGoogleWARP" = "将谷歌路由到 WARP" "xrayConfigGoogleWARP" = "将谷歌路由到 WARP"
"xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP,重启面板生效" "xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP"
"xrayConfigOpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP" "xrayConfigOpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
"xrayConfigOpenAIWARPDesc" = "将OpenAIChatGPT路由添加到WARP,重启面板生效" "xrayConfigOpenAIWARPDesc" = "将OpenAIChatGPT路由添加到WARP"
"xrayConfigNetflixWARP" = "将 Netflix 路由到 WARP" "xrayConfigNetflixWARP" = "将 Netflix 路由到 WARP"
"xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP,重启面板生效" "xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP"
"xrayConfigSpotifyWARP" = "将 Spotify 路由到 WARP" "xrayConfigSpotifyWARP" = "将 Spotify 路由到 WARP"
"xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP,重启面板生效" "xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP"
"xrayConfigIRWARP" = "将伊朗域名路由到 WARP" "xrayConfigIRWARP" = "将伊朗域名路由到 WARP"
"xrayConfigIRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效" "xrayConfigIRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
"xrayConfigInbounds" = "入站配置" "xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效" "xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端"
"xrayConfigOutbounds" = "出站配置" "xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效" "xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"xrayConfigRoutings" = "路由规则配置" "xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效" "xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
"manualLists" = "手动列表"
"manualListsDesc" = "请使用 JSON 数组格式"
"manualBlockedIPs" = "被阻止的 IP 列表"
"manualBlockedDomains" = "被阻止的域列表"
"manualDirectIPs" = "直接 IP 列表"
"manualDirectDomains" = "直接域列表"
[pages.settings.security] [pages.settings.security]
"admin" = "行政" "admin" = "行政"

View File

@@ -83,7 +83,7 @@ type Server struct {
index *controller.IndexController index *controller.IndexController
server *controller.ServerController server *controller.ServerController
xui *controller.XUIController panel *controller.XUIController
api *controller.APIController api *controller.APIController
sub *controller.SUBController sub *controller.SUBController
@@ -147,6 +147,28 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
return t, nil return t, nil
} }
func redirectMiddleware(basePath string) gin.HandlerFunc {
return func(c *gin.Context) {
// Redirect from old '/xui' path to '/panel'
path := c.Request.URL.Path
redirects := map[string]string{
"panel/API": "panel/api",
"xui/API": "panel/api",
"xui": "panel",
}
for from, to := range redirects {
from, to = basePath+from, basePath+to
if strings.HasPrefix(path, from) {
newPath := to + path[len(from):]
c.Redirect(http.StatusMovedPermanently, newPath)
c.Abort()
return
}
}
c.Next()
}
}
func (s *Server) initRouter() (*gin.Engine, error) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { if config.IsDebug() {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@@ -203,11 +225,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
} }
// Apply the redirect middleware (`/xui` to `/panel`)
engine.Use(redirectMiddleware(basePath))
g := engine.Group(basePath) g := engine.Group(basePath)
s.index = controller.NewIndexController(g) s.index = controller.NewIndexController(g)
s.server = controller.NewServerController(g) s.server = controller.NewServerController(g)
s.xui = controller.NewXUIController(g) s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g) s.api = controller.NewAPIController(g)
s.sub = controller.NewSUBController(g) s.sub = controller.NewSUBController(g)

87
x-ui.sh
View File

@@ -42,7 +42,7 @@ if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
fi fi
@@ -432,8 +432,7 @@ show_xray_status() {
} }
open_ports() { open_ports() {
if ! command -v ufw &> /dev/null if ! command -v ufw &>/dev/null; then
then
echo "ufw firewall is not installed. Installing now..." echo "ufw firewall is not installed. Installing now..."
sudo apt-get update sudo apt-get update
sudo apt-get install -y ufw sudo apt-get install -y ufw
@@ -460,22 +459,23 @@ open_ports() {
# Check if the input is valid # Check if the input is valid
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2; exit 1 echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
exit 1
fi fi
# Open the specified ports using ufw # Open the specified ports using ufw
IFS=',' read -ra PORT_LIST <<< "$ports" IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then if [[ $port == *-* ]]; then
# Split the range into start and end ports # Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1) start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2) end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and open each port # Loop through the range and open each port
for ((i=start_port; i<=end_port; i++)); do for ((i = start_port; i <= end_port; i++)); do
sudo ufw allow $i sudo ufw allow $i
done done
else else
sudo ufw allow "$port" sudo ufw allow "$port"
fi fi
done done
@@ -529,7 +529,7 @@ ssl_cert_issue() {
fi fi
fi fi
#install socat second #install socat second
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]]; then
yum install socat -y yum install socat -y
else else
apt install socat -y apt install socat -y
@@ -555,16 +555,16 @@ ssl_cert_issue() {
else else
LOGI "your domain is ready for issuing cert now..." LOGI "your domain is ready for issuing cert now..."
fi fi
#create a directory for install cert #create a directory for install cert
certPath="/root/cert/${domain}" certPath="/root/cert/${domain}"
if [ ! -d "$certPath" ]; then if [ ! -d "$certPath" ]; then
mkdir -p "$certPath" mkdir -p "$certPath"
else else
rm -rf "$certPath" rm -rf "$certPath"
mkdir -p "$certPath" mkdir -p "$certPath"
fi fi
#get needed port here #get needed port here
local WebPort=80 local WebPort=80
read -p "please choose which port do you use,default will be 80 port:" WebPort read -p "please choose which port do you use,default will be 80 port:" WebPort
@@ -595,22 +595,21 @@ ssl_cert_issue() {
else else
LOGI "install certs succeed,enable auto renew..." LOGI "install certs succeed,enable auto renew..."
fi fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade ~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:" LOGE "auto renew failed, certs details:"
ls -lah cert/* ls -lah cert/*
chmod 755 $certPath/* chmod 755 $certPath/*
exit 1 exit 1
else else
LOGI "auto renew succeed, certs details:" LOGI "auto renew succeed, certs details:"
ls -lah cert/* ls -lah cert/*
chmod 755 $certPath/* chmod 755 $certPath/*
fi fi
} }
warp_fixchatgpt() { warp_fixchatgpt() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
echo "" echo ""
@@ -619,21 +618,21 @@ warp_fixchatgpt() {
run_speedtest() { run_speedtest() {
# Check if Speedtest is already installed # Check if Speedtest is already installed
if ! command -v speedtest &> /dev/null; then if ! command -v speedtest &>/dev/null; then
# If not installed, install it # If not installed, install it
if command -v dnf &> /dev/null; then if command -v dnf &>/dev/null; then
sudo dnf install -y curl sudo dnf install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo dnf install -y speedtest sudo dnf install -y speedtest
elif command -v yum &> /dev/null; then elif command -v yum &>/dev/null; then
sudo yum install -y curl sudo yum install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo yum install -y speedtest sudo yum install -y speedtest
elif command -v apt-get &> /dev/null; then elif command -v apt-get &>/dev/null; then
sudo apt-get update && sudo apt-get install -y curl sudo apt-get update && sudo apt-get install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt-get install -y speedtest sudo apt-get install -y speedtest
elif command -v apt &> /dev/null; then elif command -v apt &>/dev/null; then
sudo apt update && sudo apt install -y curl sudo apt update && sudo apt install -y curl
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt install -y speedtest sudo apt install -y speedtest
@@ -647,8 +646,6 @@ run_speedtest() {
speedtest speedtest
} }
show_usage() { show_usage() {
echo "x-ui control menu usages: " echo "x-ui control menu usages: "
echo "------------------------------------------" echo "------------------------------------------"
@@ -760,7 +757,7 @@ show_menu() {
19) 19)
warp_fixchatgpt warp_fixchatgpt
;; ;;
20) 20)
run_speedtest run_speedtest
;; ;;
*) *)

View File

@@ -6,7 +6,7 @@ import (
) )
type InboundConfig struct { type InboundConfig struct {
Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串 Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
Port int `json:"port"` Port int `json:"port"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Settings json_util.RawMessage `json:"settings"` Settings json_util.RawMessage `json:"settings"`