个人优化

个人优化更改
This commit is contained in:
心隨緣動 2024-04-29 01:59:30 +08:00
parent 490048a975
commit 696ddaa068
61 changed files with 12952 additions and 3164 deletions

View file

@ -83,7 +83,7 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.11/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.10/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget ${Xray_URL}Xray-linux-64.zip wget ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip

View file

@ -20,6 +20,10 @@ case $1 in
ARCH="arm32-v6" ARCH="arm32-v6"
FNAME="armv6" FNAME="armv6"
;; ;;
s390x)
ARCH="s390x"
FNAME="s390x"
;;
*) *)
ARCH="64" ARCH="64"
FNAME="amd64" FNAME="amd64"
@ -27,7 +31,7 @@ case $1 in
esac esac
mkdir -p build/bin mkdir -p build/bin
cd build/bin cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.11/Xray-linux-${ARCH}.zip" wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.10/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"

View file

@ -22,7 +22,7 @@ RUN ./DockerInit.sh "$TARGETARCH"
# Stage: Final Image of 3x-ui # Stage: Final Image of 3x-ui
# ======================================================== # ========================================================
FROM alpine FROM alpine
ENV TZ=Asia/Tehran ENV TZ=Asia/Shanghai
WORKDIR /app WORKDIR /app
RUN apk add --no-cache --update \ RUN apk add --no-cache --update \

378
README.md
View file

@ -1,66 +1,48 @@
# 3X-UI # 3X-UI
[English](/README.md) | [Chinese](/README.zh.md) | [Español](/README.es_ES.md)
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p> <p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**An Advanced Web Panel • Built on Xray Core** **一个更好的面板 • 基于Xray Core构建**
[![](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/xeefei/3x-ui.svg)](https://github.com/xeefei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#) [![](https://img.shields.io/github/actions/workflow/status/xeefei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![GO Version](https://img.shields.io/github/go-mod/go-version/xeefei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#) [![Downloads](https://img.shields.io/github/downloads/xeefei/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)
> **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:** 此项目仅供个人学习交流,请不要用于非法目的,请不要在生产环境中使用。
**If this project is helpful to you, you may wish to give it a**:star2: **如果此项目对你有用,请给一个**:star2:
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p> ## 安装 & 升级
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
## Install & Upgrade
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/xeefei/3x-ui/master/install.sh)
``` ```
## Install Custom Version ## 安装指定版本
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.3.0`: 要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.3.0.1`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.3.0 bash <(curl -Ls https://raw.githubusercontent.com/xeefei/3x-ui/master/install.sh) v2.3.0.1
``` ```
## SSL Certificate ## SSL 认证
<details> <details>
<summary>Click for SSL Certificate</summary> <summary>点击查看 SSL 认证</summary>
### Cloudflare ### Cloudflare
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following: 管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件:
- Cloudflare registered email - Cloudflare 邮箱地址
- Cloudflare Global API Key - Cloudflare Global API Key
- The domain name has been resolved to the current server through cloudflare - 域名已通过 cloudflare 解析到当前服务器
How to get the Cloudflare Global API Key: **1:** 在终端中运行`x-ui` 选择 `Cloudflare SSL Certificate`.
1. Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`.
2. Visit the link https://dash.cloudflare.com/profile/api-tokens
3. Click on View Global API Key (See the screenshot below)
![](media/APIKey1.PNG)
4. You may have to re-authenticate your account. After that, the API Key will be shown (See the screenshot below)\
![](media/APIKey2.png)
When using, just enter `domain name`, `email`, `API KEY`, the diagram is as follows:
![](media/DetailEnter.png)
### Certbot ### Certbot
``` ```
@ -69,18 +51,18 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
certbot renew --dry-run certbot renew --dry-run
``` ```
***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.* ***Tip:*** *管理脚本具有 Certbot 。使用 `x-ui` 命令, 选择 `SSL Certificate Management`.*
</details> </details>
## Manual Install & Upgrade ## 手动安装 & 升级
<details> <details>
<summary>Click for manual install details</summary> <summary>点击查看 手动安装 & 升级</summary>
#### Usage #### 使用
1. To download the latest version of the compressed package directly to your server, run the following command: 1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令:
```sh ```sh
ARCH=$(uname -m) ARCH=$(uname -m)
@ -96,10 +78,10 @@ case "${ARCH}" in
esac esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz wget https://github.com/xeefei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
``` ```
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui: 2. 下载压缩包后,执行以下命令安装或升级 x-ui
```sh ```sh
ARCH=$(uname -m) ARCH=$(uname -m)
@ -128,33 +110,33 @@ systemctl restart x-ui
</details> </details>
## Install with Docker ## 通过Docker安装
<details> <details>
<summary>Click for Docker details</summary> <summary>点击查看 通过Docker安装</summary>
#### Usage #### 使用
1. Install Docker: 1. 安装Docker
```sh ```sh
bash <(curl -sSL https://get.docker.com) bash <(curl -sSL https://get.docker.com)
``` ```
2. Clone the Project Repository: 2. 克隆仓库:
```sh ```sh
git clone https://github.com/MHSanaei/3x-ui.git git clone https://github.com/xeefei/3x-ui.git
cd 3x-ui cd 3x-ui
``` ```
3. Start the Service 3. 运行服务:
```sh ```sh
docker compose up -d docker compose up -d
``` ```
OR
```sh ```sh
docker run -itd \ docker run -itd \
@ -164,10 +146,10 @@ systemctl restart x-ui
--network=host \ --network=host \
--restart=unless-stopped \ --restart=unless-stopped \
--name 3x-ui \ --name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest ghcr.io/xeefei/3x-ui:latest
``` ```
update to latest version 更新至最新版本
```sh ```sh
cd 3x-ui cd 3x-ui
@ -176,7 +158,7 @@ update to latest version
docker compose up -d docker compose up -d
``` ```
remove 3x-ui from docker 从Docker中删除3x-ui
```sh ```sh
docker stop 3x-ui docker stop 3x-ui
@ -188,117 +170,113 @@ remove 3x-ui from docker
</details> </details>
## Recommended OS ## 建议使用的操作系统
- Ubuntu 20.04+ - Ubuntu 20.04+
- Debian 11+ - Debian 11+
- CentOS 8+ - CentOS 8+
- Fedora 36+ - Fedora 36+
- Arch Linux - Arch Linux
- Parch Linux
- Manjaro - Manjaro
- Armbian - Armbian
- AlmaLinux 9+ - AlmaLinux 9+
- Rocky Linux 9+ - Rocky Linux 9+
- Oracle Linux 8+ - Oracle Linux 8+
## Supported Architectures and Devices ## 支持的架构和设备
<details> <details>
<summary>Click for Supported Architectures and devices details</summary> <summary>点击查看 支持的架构和设备</summary>
Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support: 我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构:
- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly. - **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。
- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems. - **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。
- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more. - **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。
- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others. - **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。
- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture. - **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备虽然不太普遍但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。
- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones. - **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。
- **s390x**: This architecture is commonly used in IBM mainframe computers and offers high performance and reliability for enterprise workloads.
</details> </details>
## Languages ## Languages
- English - English(英语)
- Farsi - Farsi(伊朗语)
- Chinese - Chinese(中文)
- Russian - Russian(俄语)
- Vietnamese - Vietnamese(越南语)
- Spanish - Spanish(西班牙语)
- Indonesian - Indonesian (印度尼西亚语)
- Ukrainian - Ukrainian(乌克兰语)
## Features ## Features
- System Status Monitoring - 系统状态监控
- Search within all inbounds and clients - 在所有入站和客户端中搜索
- Dark/Light theme - 深色/浅色主题
- Supports multi-user and multi-protocol - 支持多用户和多协议
- Supports protocols, including VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard - 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard
- Supports XTLS native Protocols, including RPRX-Direct, Vision, REALITY - 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY
- Traffic statistics, traffic limit, expiration time limit - 流量统计、流量限制、过期时间限制
- Customizable Xray configuration templates - 可自定义的 Xray配置模板
- Supports HTTPS access panel (self-provided domain name + SSL certificate) - 支持HTTPS访问面板自建域名+SSL证书
- Supports One-Click SSL certificate application and automatic renewal - 支持一键式SSL证书申请和自动续费
- For more advanced configuration items, please refer to the panel - 更多高级配置项目请参考面板
- Fixes API routes (user setting will be created with API) - 修复了 API 路由(用户设置将使用 API 创建)
- Supports changing configs by different items provided in the panel. - 支持通过面板中提供的不同项目更改配置。
- Supports export/import database from the panel - 支持从面板导出/导入数据库
## Default Settings ## 默认设置
<details> <details>
<summary>Click for default settings details</summary> <summary>点击查看 默认设置</summary>
### Information ### 信息
- **Port:** 2053 - **端口:** 2053
- **Username & Password:** It will be generated randomly if you skip modifying. - **用户名 & 密码:** 当您跳过设置时,此项会随机生成。
- **Database Path:** - **数据库路径:**
- /etc/x-ui/x-ui.db - /etc/x-ui/x-ui.db
- **Xray Config Path:** - **Xray 配置路径:**
- /usr/local/x-ui/bin/config.json - /usr/local/x-ui/bin/config.json
- **Web Panel Path w/o Deploying SSL:** - **面板链接无SSL**
- http://ip:2053/panel - http://ip:2053/panel
- http://domain:2053/panel - http://domain:2053/panel
- **Web Panel Path w/ Deploying SSL:** - **面板链接有SSL**
- https://domain:2053/panel - https://domain:2053/panel
</details> </details>
## [WARP Configuration](https://gitlab.com/fscarmen/warp) ## [WARP 配置](https://gitlab.com/fscarmen/warp)
<details> <details>
<summary>Click for WARP configuration details</summary> <summary>点击查看 WARP 配置</summary>
#### Usage #### 使用
If you want to use routing to WARP before v2.1.0 follow steps as below: 如果要在 v2.1.0 之前使用 WARP 路由,请按照以下步骤操作:
**1.** Install WARP on **SOCKS Proxy Mode**: **1.** **SOCKS Proxy Mode** 模式中安装Wrap
```sh ```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh) bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
``` ```
**2.** If you already installed warp, you can uninstall using below command: **2.** 如果您已经安装了 warp您可以使用以下命令卸载
```sh ```sh
warp u warp u
``` ```
**3.** Turn on the config you need in panel **3.** 在面板中打开您需要的配置
Config Features: 配置:
- Block Ads - Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP - Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
@ -306,28 +284,28 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
</details> </details>
## IP Limit ## IP 限制
<details> <details>
<summary>Click for IP limit details</summary> <summary>点击查看 IP 限制</summary>
#### Usage #### 使用
**Note:** IP Limit won't work correctly when using IP Tunnel **注意:** 使用 IP 隧道时IP 限制无法正常工作。
- For versions up to `v1.6.1`: - 适用于最高 `v1.6.1`
- IP limit is built-in into the panel. - IP 限制 已被集成在面板中。
- For versions `v1.7.0` and newer: - 适用于 `v1.7.0` 以及更新的版本:
- To make IP Limit work properly, you need to install fail2ban and its required files by following these steps: - 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件:
1. Use the `x-ui` command inside the shell. 1. 使用面板内置的 `x-ui` 指令
2. Select `IP Limit Management`. 2. 选择 `IP Limit Management`.
3. Choose the appropriate options based on your needs. 3. 根据您的需要选择合适的选项。
- make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it - 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。
```sh ```sh
"log": { "log": {
@ -339,120 +317,120 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
</details> </details>
## Telegram Bot ## Telegram 机器人
<details> <details>
<summary>Click for Telegram bot details</summary> <summary>点击查看 Telegram 机器人</summary>
#### Usage #### 使用
The web panel supports daily traffic, panel login, database backup, system status, client info, and other notification and functions through the Telegram Bot. To use the bot, you need to set the bot-related parameters in the panel, including: Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括:
- Telegram Token - 电报令牌
- Admin Chat ID(s) - 管理员聊天 ID
- Notification Time (in cron syntax) - 通知时间cron 语法)
- Expiration Date Notification - 到期日期通知
- Traffic Cap Notification - 流量上限通知
- Database Backup - 数据库备份
- CPU Load Notification - CPU 负载通知
**Reference syntax:** **参考:**
- `30 \* \* \* \* \*` - Notify at the 30s of each point - `30 \* \* \* \* \*` - 在每个点的 30 秒处通知
- `0 \*/10 \* \* \* \*` - Notify at the first second of each 10 minutes - `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知
- `@hourly` - Hourly notification - `@hourly` - 每小时通知
- `@daily` - Daily notification (00:00 in the morning) - `@daily` - 每天通知 (00:00)
- `@weekly` - weekly notification - `@weekly` - 每周通知
- `@every 8h` - Notify every 8 hours - `@every 8h` - 每8小时通知
### Telegram Bot Features ### Telegram Bot 功能
- Report periodic - 定期报告
- Login notification - 登录通知
- CPU threshold notification - CPU 阈值通知
- Threshold for Expiration time and Traffic to report in advance - 提前报告的过期时间和流量阈值
- Support client report menu if client's telegram username added to the user's configurations - 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously - 支持使用UUIDVMESS/VLESS或密码TROJAN搜索报文流量报告 - 匿名
- Menu based bot - 基于菜单的机器人
- Search client by email ( only admin ) - 通过电子邮件搜索客户端(仅限管理员)
- Check all inbounds - 检查所有入库
- Check server status - 检查服务器状态
- Check depleted users - 检查耗尽的用户
- Receive backup by request and in periodic reports - 根据请求和定期报告接收备份
- Multi language bot - 多语言机器人
### Setting up Telegram bot ### 注册 Telegram bot
- Start [Botfather](https://t.me/BotFather) in your Telegram account: - 与 [Botfather](https://t.me/BotFather) 对话:
![Botfather](./media/botfather.png) ![Botfather](./media/botfather.png)
- Create a new Bot using /newbot command: It will ask you 2 questions, A name and a username for your bot. Note that the username has to end with the word "bot". - 使用 /newbot 创建新机器人你需要提供机器人名称以及用户名注意名称中末尾要包含“bot”
![Create new bot](./media/newbot.png) ![创建机器人](./media/newbot.png)
- Start the bot you've just created. You can find the link to your bot here. - 启动您刚刚创建的机器人。可以在此处找到机器人的链接。
![token](./media/token.png) ![令牌](./media/token.png)
- Enter your panel and config Telegram bot settings like below: - 输入您的面板并配置 Telegram 机器人设置,如下所示:
![Panel Config](./media/panel-bot-config.png) ![面板设置](./media/panel-bot-config.png)
Enter your bot token in input field number 3. 在输入字段编号 3 中输入机器人令牌。
Enter the user ID in input field number 4. The Telegram accounts with this id will be the bot admin. (You can enter more than one, Just separate them with ,) 在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可)
- How to get Telegram user ID? Use this [bot](https://t.me/useridinfobot), Start the bot and it will give you the Telegram user ID. - 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot) 启动机器人,它会给你 Telegram 用户 ID。
![User ID](./media/user-id.png) ![用户 ID](./media/user-id.png)
</details> </details>
## API Routes ## API 路由
<details> <details>
<summary>Click for API routes details</summary> <summary>点击查看 API 路由</summary>
#### Usage #### 使用
- `/login` with `POST` user data: `{username: '', password: ''}` for login - `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录
- `/panel/api/inbounds` base for following actions: - `/panel/api/inbounds` 以下操作的基础:
| Method | Path | Action | | 方法 | 路径 | 操作 |
| :----: | ---------------------------------- | ------------------------------------------- | | :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds | | `GET` | `"/list"` | 获取所有入站 |
| `GET` | `"/get/:id"` | Get inbound with inbound.id | | `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email | | `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins | | `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
| `POST` | `"/add"` | Add inbound | | `POST` | `"/add"` | 添加入站 |
| `POST` | `"/del/:id"` | Delete Inbound | | `POST` | `"/del/:id"` | 删除入站 |
| `POST` | `"/update/:id"` | Update Inbound | | `POST` | `"/update/:id"` | 更新入站 |
| `POST` | `"/clientIps/:email"` | Client Ip address | | `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address | | `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
| `POST` | `"/addClient"` | Add Client to inbound | | `POST` | `"/addClient"` | 将客户端添加到入站 |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* | | `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* | | `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic | | `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds | | `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound | | `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) | | `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 -1 all |
| `POST` | `"/onlines"` | Get Online users ( list of emails ) | | `POST` | `"/onlines"` | 获取在线用户 电子邮件列表 |
\*- The field `clientId` should be filled by: \*- `clientId` 项应该使用下列数据
- `client.id` for VMESS and VLESS - `client.id` VMESS and VLESS
- `client.password` for TROJAN - `client.password` TROJAN
- `client.email` for Shadowsocks - `client.email` Shadowsocks
- [API Documentation](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm) - [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9) - [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details> </details>
## Environment Variables ## 环境变量
<details> <details>
<summary>Click for environment variables details</summary> <summary>点击查看 环境变量</summary>
#### Usage #### Usage
| Variable | Type | Default | | 变量 | Type | 默认 |
| -------------- | :--------------------------------------------: | :------------ | | -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` | | XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` | | XUI_DEBUG | `boolean` | `false` |
@ -460,7 +438,7 @@ Enter the user ID in input field number 4. The Telegram accounts with this id wi
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` | | XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` | | XUI_LOG_FOLDER | `string` | `"/var/log"` |
Example: 例子:
```sh ```sh
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
@ -468,7 +446,7 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
</details> </details>
## Preview ## 预览
![1](./media/1.png) ![1](./media/1.png)
![2](./media/2.png) ![2](./media/2.png)
@ -478,15 +456,15 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
![6](./media/6.png) ![6](./media/6.png)
![7](./media/7.png) ![7](./media/7.png)
## A Special Thanks to ## 特别感谢
- [alireza0](https://github.com/alireza0/) - [alireza0](https://github.com/alireza0/)
## Acknowledgment ## 致谢
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._ - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ - [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
## Stargazers over Time ## Star 趋势
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui) [![Stargazers over time](https://starchart.cc/xeefei/3x-ui.svg)](https://starchart.cc/xeefei/3x-ui)

View file

@ -1 +1 @@
2.3.0 2.3.0.1

View file

@ -3,7 +3,7 @@ version: "3"
services: services:
3x-ui: 3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest image: ghcr.io/xeefei/3x-ui:latest
container_name: 3x-ui container_name: 3x-ui
hostname: yourhostname hostname: yourhostname
volumes: volumes:

12
go.mod
View file

@ -3,8 +3,8 @@ module x-ui
go 1.22.2 go 1.22.2
require ( require (
github.com/Calidity/gin-sessions v1.3.1
github.com/gin-contrib/gzip v1.0.0 github.com/gin-contrib/gzip v1.0.0
github.com/gin-contrib/sessions v1.0.0
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/mymmrac/telego v0.29.2 github.com/mymmrac/telego v0.29.2
@ -14,7 +14,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.24.3 github.com/shirou/gopsutil/v3 v3.24.3
github.com/valyala/fasthttp v1.52.0 github.com/valyala/fasthttp v1.52.0
github.com/xtls/xray-core v1.8.11 github.com/xtls/xray-core v1.8.10
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
google.golang.org/grpc v1.63.2 google.golang.org/grpc v1.63.2
@ -27,7 +27,7 @@ require (
github.com/bytedance/sonic v1.11.3 // indirect github.com/bytedance/sonic v1.11.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/fasthttp/router v1.5.0 // indirect github.com/fasthttp/router v1.5.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect github.com/francoispqt/gojay v1.2.13 // indirect
@ -60,7 +60,7 @@ require (
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/quic-go/quic-go v0.42.0 // indirect github.com/quic-go/quic-go v0.42.0 // indirect
github.com/refraction-networking/utls v1.6.4 // indirect github.com/refraction-networking/utls v1.6.3 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagernet/sing v0.3.8 // indirect github.com/sagernet/sing v0.3.8 // indirect
@ -94,6 +94,6 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
lukechampine.com/blake3 v1.2.2 // indirect lukechampine.com/blake3 v1.2.1 // indirect
) )

34
go.sum
View file

@ -10,8 +10,8 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I= github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
@ -30,8 +30,8 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -53,8 +53,6 @@ github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.0.0 h1:UKN586Po/92IDX6ie5CWLgMI81obiIp5nSP85T3wlTk= github.com/gin-contrib/gzip v1.0.0 h1:UKN586Po/92IDX6ie5CWLgMI81obiIp5nSP85T3wlTk=
github.com/gin-contrib/gzip v1.0.0/go.mod h1:CtG7tQrPB3vIBo6Gat9FVUsis+1emjvQqd66ME5TdnE= github.com/gin-contrib/gzip v1.0.0/go.mod h1:CtG7tQrPB3vIBo6Gat9FVUsis+1emjvQqd66ME5TdnE=
github.com/gin-contrib/sessions v1.0.0 h1:r5GLta4Oy5xo9rAwMHx8B4wLpeRGHMdz9NafzJAdP8Y=
github.com/gin-contrib/sessions v1.0.0/go.mod h1:DN0f4bvpqMQElDdi+gNGScrP2QEI04IErRyMFyorUOI=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@ -157,8 +155,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -196,8 +194,8 @@ github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7q
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/refraction-networking/utls v1.6.4 h1:aeynTroaYn7y+mFtqv8D0bQ4bw0y9nJHneGxJ7lvRDM= github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
github.com/refraction-networking/utls v1.6.4/go.mod h1:2VL2xfiqgFAZtJKeUTlf+PSYFs3Eu7km0gCtXJ3m8zs= github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@ -284,8 +282,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3Er/cH3A9Nz3CU2KwxwOQZIePI= github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3Er/cH3A9Nz3CU2KwxwOQZIePI=
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE= github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
github.com/xtls/xray-core v1.8.11 h1:kcP9sV75/Ckk1DfRIMgZjYFA2vuYtQILCJl8Xei3pFY= github.com/xtls/xray-core v1.8.10 h1:qxae6gSteonpPI7EZyOyqw5HmRVRzmU07qs0l1GNqz4=
github.com/xtls/xray-core v1.8.11/go.mod h1:CVRl+YN8jZxEpnhFRIj3wGmEfGsj4EQDWG4lDKyFRPk= github.com/xtls/xray-core v1.8.10/go.mod h1:Mc1t+kLBPE5a1EpsUNKjMLviGz3Y0XywxeEraJZAMlI=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@ -332,8 +330,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -409,13 +407,13 @@ gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATa
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE= gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.2.2 h1:wEAbSg0IVU4ih44CVlpMqMZMpzr5hf/6aqodLlevd/w= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.2/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=

View file

@ -8,7 +8,7 @@ plain='\033[0m'
cur_dir=$(pwd) cur_dir=$(pwd)
# check root # check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1 [[ $EUID -ne 0 ]] && echo -e "${red}致命错误: ${plain} 请使用 root 权限运行此脚本\n" && exit 1
# Check OS and set release variable # Check OS and set release variable
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
@ -18,74 +18,70 @@ elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release source /usr/lib/os-release
release=$ID release=$ID
else else
echo "Failed to check the system OS, please contact the author!" >&2 echo "检查服务器操作系统失败,请联系作者!" >&2
exit 1 exit 1
fi fi
echo "The OS release is: $release" echo "目前服务器的操作系统为: $release"
arch() { arch() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;; x86_64 | x64 | amd64 ) echo 'amd64' ;;
i*86 | x86) echo '386' ;; i*86 | x86 ) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; armv8* | armv8 | arm64 | aarch64 ) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;; armv7* | armv7 | arm ) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;; armv6* | armv6 ) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;; armv5* | armv5 ) echo 'armv5' ;;
s390x) echo 's390x' ;; armv5* | armv5 ) echo 's390x' ;;
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; *) echo -e "${green}不支持的CPU架构! ${plain}" && rm -f install.sh && exit 1 ;;
esac esac
} }
echo "arch: $(arch)" echo "架构: $(arch)"
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1) os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
if [[ "${release}" == "arch" ]]; then if [[ "${release}" == "arch" ]]; then
echo "Your OS is Arch Linux" echo "您的操作系统是 ArchLinux"
elif [[ "${release}" == "parch" ]]; then
echo "Your OS is Parch linux"
elif [[ "${release}" == "manjaro" ]]; then elif [[ "${release}" == "manjaro" ]]; then
echo "Your OS is Manjaro" echo "您的操作系统是 Manjaro"
elif [[ "${release}" == "armbian" ]]; then elif [[ "${release}" == "armbian" ]]; then
echo "Your OS is Armbian" echo "您的操作系统是 Armbian"
elif [[ "${release}" == "centos" ]]; then elif [[ "${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} 请使用 CentOS 8 或更高版本 ${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} 请使用 Ubuntu 20 或更高版本!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "fedora" ]]; then elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then if [[ ${os_version} -lt 36 ]]; then
echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1 echo -e "${red} 请使用 Fedora 36 或更高版本!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "debian" ]]; then elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 11 ]]; then if [[ ${os_version} -lt 11 ]]; then
echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1 echo -e "${red} 请使用 Debian 11 或更高版本 ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "almalinux" ]]; then elif [[ "${release}" == "almalinux" ]]; then
if [[ ${os_version} -lt 9 ]]; then if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1 echo -e "${red} 请使用 AlmaLinux 9 或更高版本 ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "rocky" ]]; then elif [[ "${release}" == "rocky" ]]; then
if [[ ${os_version} -lt 9 ]]; then if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1 echo -e "${red} 请使用 RockyLinux 9 或更高版本 ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "oracle" ]]; then elif [[ "${release}" == "oracle" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1 echo -e "${red} 请使用 Oracle Linux 8 或更高版本 ${plain}\n" && exit 1
fi fi
else else
echo -e "${red}Your operating system is not supported by this script.${plain}\n" echo -e "${red}此脚本不支持您的操作系统。${plain}\n"
echo "Please ensure you are using one of the following supported operating systems:" echo "请确保您使用的是以下受支持的操作系统之一:"
echo "- Ubuntu 20.04+" echo "- Ubuntu 20.04+"
echo "- Debian 11+" echo "- Debian 11+"
echo "- CentOS 8+" echo "- CentOS 8+"
echo "- Fedora 36+" echo "- Fedora 36+"
echo "- Arch Linux" echo "- Arch Linux"
echo "- Parch Linux"
echo "- Manjaro" echo "- Manjaro"
echo "- Armbian" echo "- Armbian"
echo "- AlmaLinux 9+" echo "- AlmaLinux 9+"
@ -103,7 +99,7 @@ install_base() {
fedora) fedora)
dnf -y update && dnf install -y -q wget curl tar tzdata dnf -y update && dnf install -y -q wget curl tar tzdata
;; ;;
arch | manjaro | parch) arch | manjaro)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
;; ;;
*) *)
@ -114,34 +110,34 @@ install_base() {
# This function will be called when user installed x-ui out of security # This function will be called when user installed x-ui out of security
config_after_install() { config_after_install() {
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}安装/更新完成! 为了您的面板安全,建议修改面板设置 ${plain}"
read -p "Do you want to continue with the modification [y/n]?": config_confirm read -p "想继续修改吗 [y/n]?": config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up your username:" config_account read -p "请设置您的用户名: " config_account
echo -e "${yellow}Your username will be:${config_account}${plain}" echo -e "${yellow}您的用户名将是: ${config_account}${plain}"
read -p "Please set up your password:" config_password read -p "请设置您的密码: " config_password
echo -e "${yellow}Your password will be:${config_password}${plain}" echo -e "${yellow}您的密码将是: ${config_password}${plain}"
read -p "Please set up the panel port:" config_port read -p "请设置面板端口: " config_port
echo -e "${yellow}Your panel port is:${config_port}${plain}" echo -e "${yellow}您的面板端口号为: ${config_port}${plain}"
echo -e "${yellow}Initializing, please wait...${plain}" echo -e "${yellow}正在初始化,请稍候...${plain}"
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
echo -e "${yellow}Account name and password set successfully!${plain}" echo -e "${yellow}用户名和密码设置成功!${plain}"
/usr/local/x-ui/x-ui setting -port ${config_port} /usr/local/x-ui/x-ui setting -port ${config_port}
echo -e "${yellow}Panel port set successfully!${plain}" echo -e "${yellow}面板端口号设置成功!${plain}"
else else
echo -e "${red}cancel...${plain}" echo -e "${red}cancel...${plain}"
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
local usernameTemp=$(head -c 6 /dev/urandom | base64) local usernameTemp=$(head -c 6 /dev/urandom | base64)
local passwordTemp=$(head -c 6 /dev/urandom | base64) local passwordTemp=$(head -c 6 /dev/urandom | base64)
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp} /usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
echo -e "this is a fresh installation,will generate random login info for security concerns:" echo -e "检测到为全新安装,出于安全考虑将生成随机登录信息:"
echo -e "###############################################" echo -e "###############################################"
echo -e "${green}username:${usernameTemp}${plain}" echo -e "${green}用户名: ${usernameTemp}${plain}"
echo -e "${green}password:${passwordTemp}${plain}" echo -e "${green}密 码: ${passwordTemp}${plain}"
echo -e "###############################################" echo -e "###############################################"
echo -e "${red}if you forgot your login info,you can type x-ui and then type 8 to check after installation${plain}" echo -e "${red} 如果您忘记了登录信息,可以在安装后输入 x-ui 然后输入 8 选项进行检查 ${plain}"
else else
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 8 to check${plain}" echo -e "${red} 这是您的升级,将保留旧设置,如果您忘记了登录信息,您可以输入 x-ui 然后输入 8 选项进行检查 ${plain}"
fi fi
fi fi
/usr/local/x-ui/x-ui migrate /usr/local/x-ui/x-ui migrate
@ -151,24 +147,24 @@ install_x-ui() {
cd /usr/local/ cd /usr/local/
if [ $# == 0 ]; then if [ $# == 0 ]; then
last_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') last_version=$(curl -Ls "https://api.github.com/repos/xeefei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$last_version" ]]; then if [[ ! -n "$last_version" ]]; then
echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}" echo -e "${red}获取 x-ui 版本失败,可能是 Github API 限制,请稍后再试${plain}"
exit 1 exit 1
fi fi
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..." echo -e "获取 x-ui 最新版本:${last_version},开始安装..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/xeefei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}" echo -e "${red}下载 x-ui 失败, 请检查服务器是否可以连接至 GitHub ${plain}"
exit 1 exit 1
fi fi
else else
last_version=$1 last_version=$1
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz" url="https://github.com/xeefei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch).tar.gz"
echo -e "Beginning to install x-ui $1" echo -e "开始安装 x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url} wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}" echo -e "${red}下载 x-ui $1 失败, 请检查此版本是否存在 ${plain}"
exit 1 exit 1
fi fi
fi fi
@ -191,7 +187,7 @@ install_x-ui() {
chmod +x x-ui bin/xray-linux-$(arch) chmod +x x-ui bin/xray-linux-$(arch)
cp -f x-ui.service /etc/systemd/system/ cp -f x-ui.service /etc/systemd/system/
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/xeefei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh chmod +x /usr/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
@ -199,25 +195,40 @@ install_x-ui() {
systemctl daemon-reload systemctl daemon-reload
systemctl enable x-ui systemctl enable x-ui
systemctl start x-ui systemctl start x-ui
echo -e "${green}x-ui ${last_version}${plain} installation finished, it is running now..."
systemctl stop warp-go >/dev/null 2>&1
wg-quick down wgcf >/dev/null 2>&1
ipv4=$(curl -s4m8 ip.p3terx.com -k | sed -n 1p)
ipv6=$(curl -s6m8 ip.p3terx.com -k | sed -n 1p)
systemctl start warp-go >/dev/null 2>&1
wg-quick up wgcf >/dev/null 2>&1
echo -e "${green}x-ui ${last_version}${plain} 安装成功,正在启动..."
echo -e "" echo -e ""
echo -e "x-ui control menu usages: " echo -e "x-ui 控制菜单用法: "
echo -e "----------------------------------------------" echo -e "----------------------------------------------"
echo -e "x-ui - Enter Admin menu" echo -e "x-ui - 进入管理脚本"
echo -e "x-ui start - Start x-ui" echo -e "x-ui start - 启动 x-ui"
echo -e "x-ui stop - Stop x-ui" echo -e "x-ui stop - 关闭 x-ui"
echo -e "x-ui restart - Restart x-ui" echo -e "x-ui restart - 重启 x-ui"
echo -e "x-ui status - Show x-ui status" echo -e "x-ui status - 查看 x-ui 状态"
echo -e "x-ui enable - Enable x-ui on system startup" echo -e "x-ui enable - 启用 x-ui 开机启动"
echo -e "x-ui disable - Disable x-ui on system startup" echo -e "x-ui disable - 禁用 x-ui 开机启动"
echo -e "x-ui log - Check x-ui logs" echo -e "x-ui log - 查看 x-ui 运行日志"
echo -e "x-ui banlog - Check Fail2ban ban logs" echo -e "x-ui banlog - 检查 Fail2ban 禁止日志"
echo -e "x-ui update - Update x-ui" echo -e "x-ui update - 更新 x-ui"
echo -e "x-ui install - Install x-ui" echo -e "x-ui install - 安装 x-ui"
echo -e "x-ui uninstall - Uninstall x-ui" echo -e "x-ui uninstall - 卸载 x-ui"
echo -e "----------------------------------------------" echo -e "----------------------------------------------"
echo ""
if [[ -n $ipv4 ]]; then
echo -e "${yellow}面板 IPv4 访问地址为:${plain}${green}http://$ipv4:$config_port${plain}"
fi
if [[ -n $ipv6 ]]; then
echo -e "${yellow}面板 IPv6 访问地址为:${plain}${green}http://[$ipv6]:$config_port${plain}"
fi
echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保${plain}${red} $config_port ${plain}${yellow}端口已放行${plain}"
} }
echo -e "${green}Running...${plain}"
install_base install_base
install_x-ui $1 install_x-ui $1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because one or more lines are too long

View file

@ -690,12 +690,12 @@ class Outbound extends CommonClass {
url.searchParams.get('quicSecurity') ?? 'none', url.searchParams.get('quicSecurity') ?? 'none',
url.searchParams.get('key') ?? '', url.searchParams.get('key') ?? '',
headerType ?? 'none'); headerType ?? 'none');
} else if (type === 'grpc') { } else if (type === 'grpc') {
stream.grpc = new GrpcStreamSettings( stream.grpc = new GrpcStreamSettings(
url.searchParams.get('serviceName') ?? '', url.searchParams.get('serviceName') ?? '',
url.searchParams.get('authority') ?? '', url.searchParams.get('authority') ?? '',
url.searchParams.get('mode') == 'multi'); url.searchParams.get('mode') == 'multi');
} else if (type === 'httpupgrade') { } else if (type === 'httpupgrade') {
stream.httpupgrade = new HttpUpgradeStreamSettings(path,host); stream.httpupgrade = new HttpUpgradeStreamSettings(path,host);
} }

View file

@ -41,7 +41,7 @@ class AllSetting {
this.subJsonMux = ""; this.subJsonMux = "";
this.subJsonRules = ""; this.subJsonRules = "";
this.timeLocation = "Asia/Tehran"; this.timeLocation = "Asia/Shanghai";
if (data == null) { if (data == null) {
return return

View file

@ -759,8 +759,8 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false,xver = 0, show = false,xver = 0,
dest = 'yahoo.com:443', dest = 'sega.com:443',
serverNames = 'yahoo.com,www.yahoo.com', serverNames = 'sega.com,www.sega.com',
privateKey = '', privateKey = '',
minClient = '', minClient = '',
maxClient = '', maxClient = '',

View file

@ -1,32 +1,32 @@
{{define "head"}} {{define "head"}}
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css"> <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css"> <link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<style> <style>
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
/* vazirmatn-regular - arabic_latin_latin-ext */ /* vazirmatn-regular - arabic_latin_latin-ext */
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Vazirmatn'; font-family: 'Vazirmatn';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2'); src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039; unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol'; 'Segoe UI Emoji', 'Segoe UI Symbol';
} }
</style> </style>
<title>{{ .host }}-{{ i18n .title}}</title> <title>{{ .host }}-{{ i18n .title}}</title>
</head> </head>
<div id="message"></div> <div id="message"></div>
{{end}} {{end}}

View file

@ -1,165 +1,110 @@
{{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"
:dialog-style="isMobileQr ? { top: '18px' } : {}" :dialog-style="{ top: '20px' }"
:closable="true" :closable="true"
:class="themeSwitcher.currentTheme" :class="themeSwitcher.currentTheme"
:footer="null" width="fit-content"> :footer="null" width="300px">
<tr-qr-modal class="qr-modal"> <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
<template v-if="app.subSettings.enable && qrModal.subId"> {{ i18n "pages.inbounds.clickOnQRcode" }}
<tr-qr-box class="qr-box"> </a-tag>
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag> <template v-if="app.subSettings.enable && qrModal.subId">
<tr-qr-bg class="qr-bg-sub"> <a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
<tr-qr-bg-inner class="qr-bg-sub-inner"> <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas></div>
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas> <a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
</tr-qr-bg-inner> <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas></div>
</tr-qr-bg> </template>
</tr-qr-box> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<tr-qr-box class="qr-box"> <template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag> <a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
<tr-qr-bg class="qr-bg-sub"> <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas></div>
<tr-qr-bg-inner class="qr-bg-sub-inner"> </template>
<canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
</tr-qr-bg-inner>
</tr-qr-bg>
</tr-qr-box>
</template>
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
</tr-qr-modal>
</a-modal> </a-modal>
<script> <script>
const isMobileQr = window.innerWidth <= 768;
const qrModal = {
title: '',
dbInbound: new DBInbound(),
client: null,
qrcodes: [],
clipboard: null,
visible: false,
subId: '',
show: function(title = '', dbInbound, client) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.client = client;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD) {
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
this.qrcodes.push({
remark: "Peer " + (index + 1),
link: l
});
});
} else {
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.qrcodes.push({
remark: l.remark,
link: l.link
});
});
}
this.visible = true;
},
close: function() {
this.visible = false;
},
};
const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal',
data: {
qrModal: qrModal,
},
methods: {
copyToClipboard(elmentId, content) {
this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
},
setQrCode(elmentId, content) {
new QRious({
element: document.querySelector('#' + elmentId),
size: 400,
value: content,
background: 'white',
backgroundAlpha: 0,
foreground: 'black',
padding: 2,
level: 'L'
});
},
genSubLink(subID) {
return app.subSettings.subURI + subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI + subID;
},
revertOverflow() {
const elements = document.querySelectorAll(".qr-tag");
elements.forEach((element) => {
element.classList.remove("tr-marquee");
element.children[0].style.animation = '';
while (element.children.length > 1) {
element.removeChild(element.lastChild);
}
});
}
},
updated() {
if (this.qrModal.visible) {
fixOverflow();
} else {
this.revertOverflow();
}
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link);
});
}
});
function fixOverflow() { const qrModal = {
const elements = document.querySelectorAll(".qr-tag"); title: '',
elements.forEach((element) => { dbInbound: new DBInbound(),
function isElementOverflowing(element) { client: null,
const overflowX = element.offsetWidth < element.scrollWidth, qrcodes: [],
overflowY = element.offsetHeight < element.scrollHeight; clipboard: null,
return overflowX || overflowY; visible: false,
} subId: '',
show: function (title = '', dbInbound, client) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.client = client;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD){
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l,index) =>{
this.qrcodes.push({
remark: "Peer " + (index+1),
link: l
});
});
} else {
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.qrcodes.push({
remark: l.remark,
link: l.link
});
});
}
this.visible = true;
},
close: function () {
this.visible = false;
},
};
function wrapContentsInMarquee(element) { const qrModalApp = new Vue({
element.classList.add("tr-marquee"); delimiters: ['[[', ']]'],
element.children[0].style.animation = `move-ltr ${ el: '#qrcode-modal',
(element.children[0].clientWidth / element.clientWidth) * 5 data: {
}s ease-in-out infinite`; qrModal: qrModal,
const marqueeText = element.children[0]; },
if (element.children.length < 2) { methods: {
for (let i = 0; i < 1; i++) { copyToClipboard(elmentId, content) {
const marqueeText = element.children[0].cloneNode(true); this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
element.children[0].after(marqueeText); text: () => content,
} });
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
},
setQrCode(elmentId, content) {
new QRious({
element: document.querySelector('#' + elmentId),
size: 400,
value: content,
background: 'white',
backgroundAlpha: 0,
foreground: 'black',
padding: 2,
level: 'L'
});
},
genSubLink(subID) {
return app.subSettings.subURI+subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
}
},
updated() {
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link);
});
} }
}
if (isElementOverflowing(element)) {
wrapContentsInMarquee(element);
}
}); });
}
</script> </script>
{{end}} {{end}}

View file

@ -400,7 +400,7 @@
</g> </g>
</svg> </svg>
</div> </div>
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto; overflow-x: hidden;"> <a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;"> <a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col style="width: 100%;"> <a-col style="width: 100%;">
@ -461,7 +461,7 @@
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered"> <a-row justify="center" class="centered">
<theme-switch-login></theme-switch-login> <theme-switch></theme-switch>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -476,82 +476,83 @@
{{template "component/themeSwitcher" .}} {{template "component/themeSwitcher" .}}
{{template "component/password" .}} {{template "component/password" .}}
<script> <script>
class User { class User {
constructor() { constructor() {
this.username = ""; this.username = "";
this.password = ""; this.password = "";
}
}
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
themeSwitcher,
loading: false,
user: new User(),
secretEnable: false,
lang: ""
},
async created() {
this.lang = getLang();
this.secretEnable = await this.getSecretStatus();
},
methods: {
async login() {
this.loading = true;
const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) {
location.href = basePath + 'panel/';
} }
}, }
async getSecretStatus() {
this.loading = true; const app = new Vue({
const msg = await HttpUtil.post('/getSecretStatus'); delimiters: ['[[', ']]'],
this.loading = false; el: '#app',
if (msg.success) { data: {
this.secretEnable = msg.obj; themeSwitcher,
return msg.obj; loading: false,
user: new User(),
secretEnable: false,
lang: ""
},
async created() {
this.lang = getLang();
this.secretEnable = await this.getSecretStatus();
},
methods: {
async login() {
this.loading = true;
const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) {
location.href = basePath + 'panel/';
}
},
async getSecretStatus() {
this.loading = true;
const msg = await HttpUtil.post('/getSecretStatus');
this.loading = false;
if (msg.success) {
this.secretEnable = msg.obj;
return msg.obj;
}
},
},
});
document.addEventListener("DOMContentLoaded", function() {
var animationDelay = 2000;
initHeadline();
function initHeadline() {
animateHeadline(document.querySelectorAll('.headline'));
} }
},
},
});
document.addEventListener("DOMContentLoaded", function() {
var animationDelay = 2000;
initHeadline();
function initHeadline() { function animateHeadline(headlines) {
animateHeadline(document.querySelectorAll('.headline')); var duration = animationDelay;
} headlines.forEach(function(headline) {
setTimeout(function() {
hideWord(headline.querySelector('.is-visible'));
}, duration);
});
}
function animateHeadline(headlines) { function hideWord(word) {
var duration = animationDelay; var nextWord = takeNext(word);
headlines.forEach(function(headline) { switchWord(word, nextWord);
setTimeout(function() { setTimeout(function() {
hideWord(headline.querySelector('.is-visible')); hideWord(nextWord);
}, duration); }, animationDelay);
}); }
}
function hideWord(word) { function takeNext(word) {
var nextWord = takeNext(word); return (word.nextElementSibling) ? word.nextElementSibling : word.parentElement.firstElementChild;
switchWord(word, nextWord); }
setTimeout(function() {
hideWord(nextWord);
}, animationDelay);
}
function takeNext(word) { function switchWord(oldWord, newWord) {
return (word.nextElementSibling) ? word.nextElementSibling : word.parentElement.firstElementChild; oldWord.classList.remove('is-visible');
} oldWord.classList.add('is-hidden');
newWord.classList.remove('is-hidden');
function switchWord(oldWord, newWord) { newWord.classList.add('is-visible');
oldWord.classList.remove('is-visible'); }
oldWord.classList.add('is-hidden'); });
newWord.classList.remove('is-hidden');
newWord.classList.add('is-visible');
}
});
</script> </script>
</body> </body>
</html> </html>

View file

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

View file

@ -1,216 +1,236 @@
{{define "component/sortableTableTrigger"}} {{define "component/sortableTableTrigger"}}
<a-icon type="drag" <a-icon type="drag"
class="sortable-icon" class="sortable-icon"
style="cursor: move;" style="cursor: move;"
@mouseup="mouseUpHandler" @mouseup="mouseUpHandler"
@mousedown="mouseDownHandler" @mousedown="mouseDownHandler"
@click="clickHandler" /> @click="clickHandler" />
{{end}} {{end}}
{{define "component/sortableTable"}} {{define "component/sortableTable"}}
<script> <script>
const DRAGGABLE_ROW_CLASS = 'draggable-row'; const DRAGGABLE_ROW_CLASS = 'draggable-row';
const findParentRowElement = (el) => {
if (!el || !el.tagName) { const findParentRowElement = (el) => {
return null; if (!el || !el.tagName) {
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) { return null;
return el; } else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
} else if (el.parentNode) { return el;
return findParentRowElement(el.parentNode); } else if (el.parentNode) {
} else { return findParentRowElement(el.parentNode);
return null;
}
}
Vue.component('a-table-sortable', {
data() {
return {
sortingElementIndex: null,
newElementIndex: null,
};
},
props: ['data-source', 'customRow'],
inheritAttrs: false,
provide() {
const sortable = {}
Object.defineProperty(sortable, "setSortableIndex", {
enumerable: true,
get: () => this.setCurrentSortableIndex,
});
Object.defineProperty(sortable, "resetSortableIndex", {
enumerable: true,
get: () => this.resetSortableIndex,
});
return {
sortable,
}
},
render: function(createElement) {
return createElement('a-table', {
class: {
'ant-table-is-sorting': this.isDragging(),
},
props: {
...this.$attrs,
'data-source': this.records,
customRow: (record, index) => this.customRowRender(record, index),
},
on: this.$listeners,
nativeOn: {
drop: (e) => this.dropHandler(e),
},
scopedSlots: this.$scopedSlots,
}, this.$slots.default, )
},
created() {
this.$memoSort = {};
},
methods: {
isDragging() {
const currentIndex = this.sortingElementIndex;
return currentIndex !== null && currentIndex !== undefined;
},
resetSortableIndex(e, index) {
this.sortingElementIndex = null;
this.newElementIndex = null;
this.$memoSort = {};
},
setCurrentSortableIndex(e, index) {
this.sortingElementIndex = index;
},
dragStartHandler(e, index) {
if (!this.isDragging()) {
e.preventDefault();
return;
}
const hideDragImage = this.$el.cloneNode(true);
hideDragImage.id = "hideDragImage-hide";
hideDragImage.style.opacity = 0;
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
},
dragStopHandler(e, index) {
const hideDragImage = document.getElementById('hideDragImage-hide');
if (hideDragImage) hideDragImage.remove();
this.resetSortableIndex(e, index);
},
dragOverHandler(e, index) {
if (!this.isDragging()) {
return;
}
e.preventDefault();
const currentIndex = this.sortingElementIndex;
if (index === currentIndex) {
this.newElementIndex = null;
return;
}
const row = findParentRowElement(e.target);
if (!row) {
return;
}
const rect = row.getBoundingClientRect();
const offsetTop = e.pageY - rect.top;
if (offsetTop < rect.height / 2) {
this.newElementIndex = Math.max(index - 1, 0);
} else { } else {
this.newElementIndex = index; return null;
} }
},
dropHandler(e) {
if (this.isDragging()) {
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
}
},
customRowRender(record, index) {
const parentMethodResult = this.customRow?.(record, index) || {};
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
return {
...parentMethodResult,
attrs: {
...(parentMethodResult?.attrs || {}),
draggable: true,
},
on: {
...(parentMethodResult?.on || {}),
dragstart: (e) => this.dragStartHandler(e, index),
dragend: (e) => this.dragStopHandler(e, index),
dragover: (e) => this.dragOverHandler(e, index),
},
class: {
...(parentMethodResult?.class || {}),
[DRAGGABLE_ROW_CLASS]: true,
['dragging']: this.isDragging() ? (newIndex === null ? index === currentIndex : index === newIndex) : false,
},
};
}
},
computed: {
records() {
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
return this.dataSource;
}
if (this.$memoSort.newIndex === newIndex) {
return this.$memoSort.list;
}
let list = [...this.dataSource];
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
this.$memoSort = {
newIndex,
list,
};
return list;
}
} }
});
Vue.component('table-sort-trigger', { Vue.component('a-table-sortable', {
template: `{{template "component/sortableTableTrigger"}}`, data() {
props: ['item-index'], return {
inject: ['sortable'], sortingElementIndex: null,
methods: { newElementIndex: null,
mouseDownHandler(e) { };
if (this.sortable) { },
this.sortable.setSortableIndex(e, this.itemIndex); props: ['data-source', 'customRow'],
inheritAttrs: false,
provide() {
const sortable = {}
Object.defineProperty(sortable, "setSortableIndex", {
enumerable: true,
get: () => this.setCurrentSortableIndex,
});
Object.defineProperty(sortable, "resetSortableIndex", {
enumerable: true,
get: () => this.resetSortableIndex,
});
return {
sortable,
}
},
render: function (createElement) {
return createElement('a-table', {
class: {
'ant-table-is-sorting': this.isDragging(),
},
props: {
...this.$attrs,
'data-source': this.records,
customRow: (record, index) => this.customRowRender(record, index),
},
on: this.$listeners,
nativeOn: {
drop: (e) => this.dropHandler(e),
},
scopedSlots: this.$scopedSlots,
}, this.$slots.default, )
},
created() {
this.$memoSort = {};
},
methods: {
isDragging() {
const currentIndex = this.sortingElementIndex;
return currentIndex !== null && currentIndex !== undefined;
},
resetSortableIndex(e, index) {
this.sortingElementIndex = null;
this.newElementIndex = null;
this.$memoSort = {};
},
setCurrentSortableIndex(e, index) {
this.sortingElementIndex = index;
},
dragStartHandler(e, index) {
if (!this.isDragging()) {
e.preventDefault();
return;
}
const hideDragImage = this.$el.cloneNode(true);
hideDragImage.id = "hideDragImage-hide";
hideDragImage.style.opacity = 0;
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
},
dragStopHandler(e, index) {
const hideDragImage = document.getElementById('hideDragImage-hide');
if (hideDragImage) hideDragImage.remove();
this.resetSortableIndex(e, index);
},
dragOverHandler(e, index) {
if (!this.isDragging()) {
return;
}
e.preventDefault();
const currentIndex = this.sortingElementIndex;
if (index === currentIndex) {
this.newElementIndex = null;
return;
}
const row = findParentRowElement(e.target);
if (!row) {
return;
}
const rect = row.getBoundingClientRect();
const offsetTop = e.pageY - rect.top;
if (offsetTop < rect.height / 2) {
this.newElementIndex = Math.max(index - 1, 0);
} else {
this.newElementIndex = index;
}
},
dropHandler(e) {
if (this.isDragging()) {
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
}
},
customRowRender(record, index) {
const parentMethodResult = this.customRow?.(record, index) || {};
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
return {
...parentMethodResult,
attrs: {
...(parentMethodResult?.attrs || {}),
draggable: true,
},
on: {
...(parentMethodResult?.on || {}),
dragstart: (e) => this.dragStartHandler(e, index),
dragend: (e) => this.dragStopHandler(e, index),
dragover: (e) => this.dragOverHandler(e, index),
},
class: {
...(parentMethodResult?.class || {}),
[DRAGGABLE_ROW_CLASS]: true,
['dragging']: this.isDragging()
? (newIndex === null ? index === currentIndex : index === newIndex)
: false,
},
};
}
},
computed: {
records() {
const newIndex = this.newElementIndex;
const currentIndex = this.sortingElementIndex;
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
return this.dataSource;
}
if (this.$memoSort.newIndex === newIndex) {
return this.$memoSort.list;
}
let list = [...this.dataSource];
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
this.$memoSort = {
newIndex,
list,
};
return list;
}
} }
}, });
mouseUpHandler(e) {
if (this.sortable) { Vue.component('table-sort-trigger', {
this.sortable.resetSortableIndex(e, this.itemIndex); template: `{{template "component/sortableTableTrigger"}}`,
props: ['item-index'],
inject: ['sortable'],
methods: {
mouseDownHandler(e) {
if (this.sortable) {
this.sortable.setSortableIndex(e, this.itemIndex);
}
},
mouseUpHandler(e) {
if (this.sortable) {
this.sortable.resetSortableIndex(e, this.itemIndex);
}
},
clickHandler(e) {
e.preventDefault();
},
} }
}, })
clickHandler(e) {
e.preventDefault();
},
}
})
</script> </script>
<style> <style>
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
.sortable-icon { .sortable-icon {
display: none; display: none;
}
}
.ant-table-is-sorting .draggable-row td {
background-color: #ffffff !important;
}
.dark .ant-table-is-sorting .draggable-row td {
background-color: var(--dark-color-surface-100) !important;
}
.ant-table-is-sorting .dragging td {
background-color: rgb(232 244 242) !important;
color: rgba(0, 0, 0, 0.3);
}
.dark .ant-table-is-sorting .dragging td {
background-color: var(--dark-color-table-hover) !important;
color: rgba(255, 255, 255, 0.3);
}
.ant-table-is-sorting .dragging {
opacity: 1;
box-shadow: 1px -2px 2px #008771;
transition: all 0.2s;
}
.ant-table-is-sorting .dragging .ant-table-row-index {
opacity: 0.3;
} }
}
.ant-table-is-sorting .draggable-row td {
background-color: #ffffff !important;
}
.dark .ant-table-is-sorting .draggable-row td {
background-color: var(--dark-color-surface-100) !important;
}
.ant-table-is-sorting .dragging td {
background-color: rgb(232 244 242) !important;
color: rgba(0, 0, 0, 0.3);
}
.dark .ant-table-is-sorting .dragging td {
background-color: var(--dark-color-table-hover) !important;
color: rgba(255, 255, 255, 0.3);
}
.ant-table-is-sorting .dragging {
opacity: 1;
box-shadow: 1px -2px 2px #008771;
transition: all 0.2s;
}
.ant-table-is-sorting .dragging .ant-table-row-index {
opacity: 0.3;
}
</style> </style>
{{end}} {{end}}

View file

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

View file

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

View file

@ -1,211 +1,225 @@
{{define "form/outbound"}} {{define "form/outbound"}}
<!-- base --> <!-- base -->
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }"> <a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
<a-tab-pane key="1" tab="Form"> <a-tab-pane key="1" tab="Form">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option> <a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'"> <a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input> <a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'> <a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
<a-input v-model="outbound.sendThrough"></a-input> <a-input v-model="outbound.sendThrough"></a-input>
</a-form-item> </a-form-item>
<!-- freedom settings--> <!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom"> <template v-if="outbound.protocol === Protocols.Freedom">
<a-form-item label='Strategy'> <a-form-item label='Strategy'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option> v-model="outbound.settings.domainStrategy"
</a-select> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item label='Fragment'> <a-form-item label='Fragment'>
<a-switch :checked="Object.keys(outbound.settings.fragment).length >0" @change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}"></a-switch> <a-switch
:checked="Object.keys(outbound.settings.fragment).length >0"
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
</a-switch>
</a-form-item> </a-form-item>
<template v-if="Object.keys(outbound.settings.fragment).length >0"> <template v-if="Object.keys(outbound.settings.fragment).length >0">
<a-form-item label='Packets'> <a-form-item label='Packets'>
<a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option> v-model="outbound.settings.fragment.packets"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Length'> <a-form-item label='Length'>
<a-input v-model.trim="outbound.settings.fragment.length"></a-input> <a-input v-model.trim="outbound.settings.fragment.length"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Interval'> <a-form-item label='Interval'>
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input> <a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- blackhole settings --> <!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole"> <template v-if="outbound.protocol === Protocols.Blackhole">
<a-form-item label='Response Type'> <a-form-item label='Response Type'>
<a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select
v-model="outbound.settings.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option> <a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
<!-- dns settings --> <!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS"> <template v-if="outbound.protocol === Protocols.DNS">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select
v-model="outbound.settings.network"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option> <a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
<!-- wireguard settings --> <!-- wireguard settings -->
<template v-if="outbound.protocol === Protocols.Wireguard"> <template v-if="outbound.protocol === Protocols.Wireguard">
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template> </template>
{{ i18n "pages.xray.outbound.address" }} {{ i18n "pages.xray.outbound.address" }} <a-icon type="question-circle"></a-icon>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="outbound.settings.address"></a-input> <a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
{{ i18n "pages.xray.wireguard.secretKey" }} {{ i18n "pages.xray.wireguard.secretKey" }}
<a-icon type="sync" @click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())"></a-icon> <a-icon type="sync"
@click="[outbound.settings.pubKey, outbound.settings.secretKey] = Object.values(Wireguard.generateKeypair())">
</a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="outbound.settings.secretKey"></a-input> <a-input v-model.trim="outbound.settings.secretKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='MTU'>
<a-input-number v-model.number="outbound.settings.mtu"></a-input-number>
</a-form-item>
<a-form-item label='Workers'>
<a-input-number min="0" v-model.number="outbound.settings.workers"></a-input>
</a-form-item>
<a-form-item label='Kernel Mode'>
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template>
Reserved <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item>
<a-form-item label="Peers">
<a-button type="primary" size="small" @click="outbound.settings.addPeer()">+</a-button>
</a-form-item>
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">
Peer [[ index + 1 ]]
<a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)"
style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
<a-input v-model.trim="peer.endpoint"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'> <a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
<a-input disabled v-model="outbound.settings.pubKey"></a-input> <a-input v-model.trim="peer.publicKey"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'> <a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
<a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme"> <a-input v-model.trim="peer.psk"></a-input>
<a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='MTU'>
<a-input-number v-model.number="outbound.settings.mtu"></a-input-number>
</a-form-item>
<a-form-item label='Workers'>
<a-input-number min="0" v-model.number="outbound.settings.workers"></a-input-number>
</a-form-item>
<a-form-item label='Kernel Mode'>
<a-switch v-model="outbound.settings.kernelMode"></a-switch>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template> Reserved <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item>
<a-form-item label="Peers">
<a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button>
</a-form-item>
<a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1" type="delete" @click="() => outbound.settings.delPeer(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
<a-input v-model.trim="peer.endpoint"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
<a-input v-model.trim="peer.publicKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
<a-input v-model.trim="peer.psk"></a-input>
</a-form-item>
<a-form-item>
<template slot="label"> <template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }} {{ i18n "pages.xray.wireguard.allowedIPs" }} <a-button type="primary" size="small" @click="peer.allowedIPs.push('')">+</a-button>
<a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
</template> </template>
<template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;"> <template v-for="(aip, index) in peer.allowedIPs" style="margin-bottom: 10px;">
<a-input v-model.trim="peer.allowedIPs[index]"> <a-input v-model.trim="peer.allowedIPs[index]">
<a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)"></a-button> <a-button v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small" @click="peer.allowedIPs.splice(index, 1)">-</a-button>
</a-input> </a-input>
</template> </template>
</a-form-item> </a-form-item>
<a-form-item label='Keep Alive'> <a-form-item label='Keep Alive'>
<a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number> <a-input-number v-model.number="peer.keepAlive" :min="0"></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>
</template> </template>
<!-- Address + Port --> <!-- Address + Port -->
<template v-if="outbound.hasAddressPort()"> <template v-if="outbound.hasAddressPort()">
<a-form-item label='{{ i18n "pages.inbounds.address" }}'> <a-form-item label='{{ i18n "pages.inbounds.address" }}'>
<a-input v-model.trim="outbound.settings.address"></a-input> <a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number> <a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
<!-- Vnext (vless/vmess) settings --> <!-- Vnext (vless/vmess) settings -->
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)"> <template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<a-form-item label='ID'> <a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input> <a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item> </a-form-item>
<!-- vless settings -->
<!-- vless settings --> <template v-if="outbound.canEnableTlsFlow()">
<template v-if="outbound.canEnableTlsFlow()"> <a-form-item label='Flow'>
<a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- Servers (trojan/shadowsocks/socks/http) settings --> <!-- Servers (trojan/shadowsocks/socks/http) settings -->
<template v-if="outbound.hasServers()"> <template v-if="outbound.hasServers()">
<!-- http / socks --> <!-- http / socks -->
<template v-if="outbound.hasUsername()"> <template v-if="outbound.hasUsername()">
<a-form-item label='{{ i18n "username" }}'> <a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="outbound.settings.user"></a-input> <a-input v-model.trim="outbound.settings.user"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.settings.pass"></a-input> <a-input v-model.trim="outbound.settings.pass"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<!-- trojan/shadowsocks -->
<!-- trojan/shadowsocks --> <template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)"> <a-form-item label='{{ i18n "password" }}'>
<a-form-item label='{{ i18n "password" }}'> <a-input v-model.trim="outbound.settings.password"></a-input>
<a-input v-model.trim="outbound.settings.password"></a-input> </a-form-item>
</a-form-item> </template>
</template> <!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<!-- shadowsocks --> <a-form-item label='{{ i18n "encryption" }}'>
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option> <a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='UDP over TCP'> <a-form-item label='UDP over TCP'>
<a-switch v-model="outbound.settings.uot"></a-switch> <a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- stream settings --> <!-- stream settings -->
<template v-if="outbound.canEnableStream()"> <template v-if="outbound.canEnableStream()">
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="outbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option> <a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option> <a-select-option value="ws">WebSocket</a-select-option>
@ -213,229 +227,235 @@
<a-select-option value="quic">QUIC</a-select-option> <a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option> <a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option> <a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
</a-select> </a-select>
</a-form-item>
<template v-if="outbound.stream.network === 'tcp'">
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch :checked="outbound.stream.tcp.type === 'http'"
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
</a-form-item> </a-form-item>
<template v-if="outbound.stream.network === 'tcp'"> <template v-if="outbound.stream.tcp.type == 'http'">
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch :checked="outbound.stream.tcp.type === 'http'" @change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
</a-form-item>
<template v-if="outbound.stream.tcp.type == 'http'">
<a-form-item label='{{ i18n "host" }}'> <a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="outbound.stream.tcp.host"></a-input> <a-input v-model.trim="outbound.stream.tcp.host"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="outbound.stream.tcp.path"></a-input> <a-input v-model.trim="outbound.stream.tcp.path"></a-input>
</a-form-item> </a-form-item>
</template>
</template> </template>
</template>
<!-- kcp --> <!-- kcp -->
<template v-if="outbound.stream.network === 'kcp'"> <template v-if="outbound.stream.network === 'kcp'">
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option> <a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option> <a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option> <a-select-option value="utp">uTP</a-select-option>
<a-select-option value="wechat-video">WeChat</a-select-option> <a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option> <a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option> <a-select-option value="wireguard">WireGuard</a-select-option>
<a-select-option value="dns">DNS</a-select-option> <a-select-option value="dns">DNS</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-input v-model="outbound.stream.kcp.seed"></a-input> <a-input v-model="outbound.stream.kcp.seed"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='MTU'> <a-form-item label='MTU'>
<a-input-number v-model.number="outbound.stream.kcp.mtu"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.mtu"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='TTI (ms)'> <a-form-item label='TTI (ms)'>
<a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Uplink (MB/s)'> <a-form-item label='Uplink (MB/s)'>
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Downlink (MB/s)'> <a-form-item label='Downlink (MB/s)'>
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Congestion'> <a-form-item label='Congestion'>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch> <a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='Read Buffer (MB)'> <a-form-item label='Read Buffer (MB)'>
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='Write Buffer (MB)'> <a-form-item label='Write Buffer (MB)'>
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number> <a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
<!-- ws --> <!-- ws -->
<template v-if="outbound.stream.network === 'ws'"> <template v-if="outbound.stream.network === 'ws'">
<a-form-item label='{{ i18n "host" }}'> <a-form-item label='{{ i18n "host" }}'>
<a-input v-model="outbound.stream.ws.host"></a-input> <a-input v-model="outbound.stream.ws.host"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="outbound.stream.ws.path"></a-input> <a-input v-model.trim="outbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<!-- http --> <!-- http -->
<template v-if="outbound.stream.network === 'http'"> <template v-if="outbound.stream.network === 'http'">
<a-form-item label='{{ i18n "host" }}'> <a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="outbound.stream.http.host"></a-input> <a-input v-model.trim="outbound.stream.http.host"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="outbound.stream.http.path"></a-input> <a-input v-model.trim="outbound.stream.http.path"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<!-- quic --> <!-- quic -->
<template v-if="outbound.stream.network === 'quic'"> <template v-if="outbound.stream.network === 'quic'">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="outbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option> <a-select-option value="none">None</a-select-option>
<a-select-option value="aes-128-gcm">AES-128-GCM</a-select-option> <a-select-option value="aes-128-gcm">AES-128-GCM</a-select-option>
<a-select-option value="chacha20-poly1305">CHACHA20-POLY1305</a-select-option> <a-select-option value="chacha20-poly1305">CHACHA20-POLY1305</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.stream.quic.key"></a-input> <a-input v-model.trim="outbound.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="outbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option> <a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option> <a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option> <a-select-option value="utp">uTP</a-select-option>
<a-select-option value="wechat-video">WeChat</a-select-option> <a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option> <a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option> <a-select-option value="wireguard">WireGuard</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</template> </template>
<!-- grpc --> <!-- grpc -->
<template v-if="outbound.stream.network === 'grpc'"> <template v-if="outbound.stream.network === 'grpc'">
<a-form-item label='Service Name'> <a-form-item label='Service Name'>
<a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input> <a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Authority"> <a-form-item label="Authority">
<a-input v-model.trim="outbound.stream.grpc.authority"></a-input> <a-input v-model.trim="outbound.stream.grpc.authority"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Multi Mode'> <a-form-item label='Multi Mode'>
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch> <a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<!-- httpupgrade --> <!-- httpupgrade -->
<template v-if="outbound.stream.network === 'httpupgrade'"> <template v-if="outbound.stream.network === 'httpupgrade'">
<a-form-item label='{{ i18n "host" }}'> <a-form-item label='{{ i18n "host" }}'>
<a-input v-model="outbound.stream.httpupgrade.host"></a-input> <a-input v-model="outbound.stream.httpupgrade.host"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="outbound.stream.httpupgrade.path"></a-input> <a-input v-model.trim="outbound.stream.httpupgrade.path"></a-input>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- tls settings --> <!-- tls settings -->
<template v-if="outbound.canEnableTls()"> <template v-if="outbound.canEnableTls()">
<a-form-item label='{{ i18n "security" }}'> <a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="outbound.stream.security" button-style="solid"> <a-radio-group v-model="outbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button> <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button> <a-radio-button value="tls">TLS</a-radio-button>
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button> <a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<template v-if="outbound.stream.isTls"> <template v-if="outbound.stream.isTls">
<a-form-item label="SNI" placeholder="Server Name Indication"> <a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input> <a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.tls.fingerprint"
<a-select-option value=''>None</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</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> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ALPN"> <a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="outbound.stream.tls.alpn"> <a-select mode="multiple"
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Allow Insecure"> <a-form-item label="Allow Insecure">
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch> <a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<!-- reality settings --> <!-- reality settings -->
<template v-if="outbound.stream.isReality"> <template v-if="outbound.stream.isReality">
<a-form-item label="SNI"> <a-form-item label="SNI">
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input> <a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.reality.fingerprint"
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<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="Short ID"> <a-form-item label="Short ID">
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input> <a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="SpiderX"> <a-form-item label="SpiderX">
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input> <a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></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="outbound.stream.reality.publicKey"></a-input> <a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- sockopt settings --> <!-- sockopt settings -->
<a-form-item label="Sockopts"> <a-form-item label="Sockopts">
<a-switch v-model="outbound.stream.sockoptSwitch"></a-switch> <a-switch v-model="outbound.stream.sockoptSwitch"></a-switch>
</a-form-item> </a-form-item>
<template v-if="outbound.stream.sockoptSwitch"> <template v-if="outbound.stream.sockoptSwitch">
<a-form-item label="Dialer Proxy"> <a-form-item label="Dialer Proxy">
<a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.sockopt.dialerProxy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option> <a-select-option v-for="tag in ['', ...outModal.tags]" :value="tag">[[ tag ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="TCP Fast Open"> <a-form-item label="TCP Fast Open">
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch> <a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Keep Alive Interval"> <a-form-item label="Keep Alive Interval">
<a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number> <a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP No-Delay"> <a-form-item label="TCP No-Delay">
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch> <a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<!-- mux settings --> <!-- mux settings -->
<template v-if="outbound.canEnableMux()"> <template v-if="outbound.canEnableMux()">
<a-form-item label="Mux"> <a-form-item label="Mux">
<a-switch v-model="outbound.mux.enabled"></a-switch> <a-switch v-model="outbound.mux.enabled"></a-switch>
</a-form-item>
<template v-if="outbound.mux.enabled">
<a-form-item label="Concurrency">
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp Concurrency">
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
</a-select>
</a-form-item>
</template>
</template>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0"> Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
<a-button @click="convertLink" type="primary">
<a-icon type="form"></a-icon>
</a-button>
</a-form-item> </a-form-item>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea> <template v-if="outbound.mux.enabled">
</a-tab-pane> <a-form-item label="Concurrency">
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp Concurrency">
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
</a-select>
</a-form-item>
</template>
</template>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="JSON" force-render="true">
<a-form-item style="margin: 10px 0">
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
</a-form-item>
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
</a-tab-pane>
</a-tabs> </a-tabs>
{{end}} {{end}}

View file

@ -1,23 +1,21 @@
{{define "form/http"}} {{define "form/http"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<table style="width: 100%; text-align: center; margin: 1rem 0;"> <table style="width: 100%; text-align: center; margin: 1rem 0;">
<tr> <tr>
<td width="45%">{{ i18n "username" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "password" }}</td>
<td> <td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())">+</a-button></td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button> </tr>
</td> </table>
</tr> <a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
</table> <a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;"> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'> </a-input>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template> <a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
</a-input> <template slot="addonAfter">
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'> <a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
<template slot="addonAfter"> </template>
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button> </a-input>
</template> </a-input-group>
</a-input>
</a-input-group>
</a-form> </a-form>
{{end}} {{end}}

View file

@ -1,34 +1,33 @@
{{define "form/socks"}} {{define "form/socks"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<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 label="IP" v-if="inbound.settings.udp"> <a-form-item label="IP" v-if="inbound.settings.udp">
<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-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'" @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch> <a-switch :checked="inbound.settings.auth === 'password'"
</a-form-item> @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
<template v-if="inbound.settings.auth === 'password'"> </a-form-item>
<table style="width: 100%; text-align: center; margin: 1rem 0;"> <template v-if="inbound.settings.auth === 'password'">
<tr> <table style="width: 100%; text-align: center; margin: 1rem 0;">
<td width="45%">{{ i18n "username" }}</td> <tr>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td> <td width="45%">{{ i18n "password" }}</td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button> <td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
</td> </tr>
</tr> </table>
</table> <a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;"> <a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template> </a-input>
</a-input> <a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'> <template slot="addonAfter">
<template slot="addonAfter"> <a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button> </template>
</template> </a-input>
</a-input> </a-input-group>
</a-input-group> </template>
</template>
</a-form> </a-form>
{{end}} {{end}}

View file

@ -1,50 +1,54 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else> <a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%"> <table width="100%">
<tr class="client-table-header"> <tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th> <th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th> <th>Password</th>
</tr> </tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> <tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td> <td>[[ client.email ]]</td>
<td>[[ client.password ]]</td> <td>[[ client.password ]]</td>
</tr> </tr>
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality"> <template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- trojan fallbacks --> <!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
<a-divider style="margin:0;"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon> :wrapper-col="{ md: {span:14} }">
</a-divider> <a-divider style="margin:0;">
<a-form-item label='SNI'> Fallback [[ index + 1 ]]
<a-input v-model="fallback.name"></a-input> <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
</a-form-item> style="color: rgb(255, 77, 79);cursor: pointer;" />
<a-form-item label='ALPN'> </a-divider>
<a-input v-model="fallback.alpn"></a-input> <a-form-item label='SNI'>
</a-form-item> <a-input v-model="fallback.name"></a-input>
<a-form-item label='Path'> </a-form-item>
<a-input v-model="fallback.path"></a-input> <a-form-item label='ALPN'>
</a-form-item> <a-input v-model="fallback.alpn"></a-input>
<a-form-item label='Dest'> </a-form-item>
<a-input v-model="fallback.dest"></a-input> <a-form-item label='Path'>
</a-form-item> <a-input v-model="fallback.path"></a-input>
<a-form-item label='xVer'> </a-form-item>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number> <a-form-item label='Dest'>
</a-form-item> <a-input v-model="fallback.dest"></a-input>
</a-form> </a-form-item>
<a-divider style="margin:5px 0;"></a-divider> <a-form-item label='xVer'>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>
</template> </template>
{{end}} {{end}}

View file

@ -1,52 +1,56 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else> <a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%"> <table width="100%">
<tr class="client-table-header"> <tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th> <th>{{ i18n "pages.inbounds.email" }}</th>
<th>Flow</th> <th>Flow</th>
<th>ID</th> <th>ID</th>
</tr> </tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''"> <tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td> <td>[[ client.email ]]</td>
<td>[[ client.flow ]]</td> <td>[[ client.flow ]]</td>
<td>[[ client.id ]]</td> <td>[[ client.id ]]</td>
</tr> </tr>
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality"> <template v-if="inbound.isTcp && !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- vless fallbacks --> <!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
<a-divider style="margin:0;"> Fallback [[ index + 1 ]] <a-icon type="delete" @click="() => inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"></a-icon> :wrapper-col="{ md: {span:14} }">
</a-divider> <a-divider style="margin:0;">
<a-form-item label='SNI'> Fallback [[ index + 1 ]]
<a-input v-model="fallback.name"></a-input> <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
</a-form-item> style="color: rgb(255, 77, 79);cursor: pointer;" />
<a-form-item label='ALPN'> </a-divider>
<a-input v-model="fallback.alpn"></a-input> <a-form-item label='SNI'>
</a-form-item> <a-input v-model="fallback.name"></a-input>
<a-form-item label='Path'> </a-form-item>
<a-input v-model="fallback.path"></a-input> <a-form-item label='ALPN'>
</a-form-item> <a-input v-model="fallback.alpn"></a-input>
<a-form-item label='Dest'> </a-form-item>
<a-input v-model="fallback.dest"></a-input> <a-form-item label='Path'>
</a-form-item> <a-input v-model="fallback.path"></a-input>
<a-form-item label='xVer'> </a-form-item>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number> <a-form-item label='Dest'>
</a-form-item> <a-input v-model="fallback.dest"></a-input>
</a-form> </a-form-item>
<a-divider style="margin:5px 0;"></a-divider> <a-form-item label='xVer'>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>
</template> </template>
{{end}} {{end}}

View file

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

View file

@ -1,29 +1,26 @@
{{define "form/externalProxy"}} {{define "form/externalProxy"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:5px 0 0;"></a-divider> <a-divider style="margin:5px 0 0;"></a-divider>
<a-form-item label="External Proxy"> <a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch> <a-switch v-model="externalProxy"></a-switch>
<a-button icon="plus" v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button> <a-button v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})">+</a-button>
</a-form-item> </a-form-item>
<a-input-group style="margin: 8px 0;" compact v-for="(row, index) in inbound.stream.externalProxy"> <a-input-group style="margin: 8px 0;" compact v-for="(row, index) in inbound.stream.externalProxy">
<template> <template>
<a-tooltip title="Force TLS"> <a-tooltip title="Force TLS">
<a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option> <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
<a-select-option value="none">{{ i18n "none" }}</a-select-option> <a-select-option value="none">{{ i18n "none" }}</a-select-option>
<a-select-option value="tls">TLS</a-select-option> <a-select-option value="tls">TLS</a-select-option>
</a-select> </a-select>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input style="width: 35%" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input> <a-input style="width: 35%" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'> <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number> <a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
</a-tooltip> </a-tooltip>
<a-input style="width: 30%; top: 0;" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'> <a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
<template slot="addonAfter"> <a-button style="width: 10%; margin: 0px; border-radius: 0 1rem 1rem 0;" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
<a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button> </a-input-group>
</template>
</a-input>
</a-input-group>
</a-form> </a-form>
{{end}} {{end}}

View file

@ -1,17 +1,19 @@
{{define "form/streamHTTP"}} {{define "form/streamHTTP"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "path" }}'> <a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.http.path"></a-input> <a-input v-model.trim="inbound.stream.http.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label">{{ i18n "host" }} <template slot="label">{{ i18n "host" }}
<a-button icon="plus" size="small" @click="inbound.stream.http.addHost()"></a-button> <a-button size="small" @click="inbound.stream.http.addHost()">+</a-button>
</template> </template>
<template v-for="(host, index) in inbound.stream.http.host"> <template v-for="(host, index) in inbound.stream.http.host">
<a-input v-model.trim="inbound.stream.http.host[index]"> <a-input v-model.trim="inbound.stream.http.host[index]">
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.http.removeHost(index)" v-if="inbound.stream.http.host.length>1"></a-button> <a-button size="small" slot="addonAfter"
</a-input> @click="inbound.stream.http.removeHost(index)"
</template> v-if="inbound.stream.http.host.length>1">-</a-button>
</a-form-item> </a-input>
</template>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View file

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

View file

@ -1,72 +1,82 @@
{{define "form/streamTCP"}} {{define "form/streamTCP"}}
<!-- tcp type --> <!-- tcp type -->
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="PROXY Protocol" v-if="inbound.canEnableTls()"> <a-form-item label="PROXY Protocol" v-if="inbound.canEnableTls()">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='HTTP {{ i18n "camouflage" }}'> <a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch :checked="inbound.stream.tcp.type === 'http'" @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch> <a-switch :checked="inbound.stream.tcp.type === 'http'"
</a-form-item> @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
</a-switch>
</a-form-item>
</a-form> </a-form>
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
<!-- tcp request --> :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider> <!-- tcp request -->
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'> <a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
</a-form-item> <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.method" }}'> </a-form-item>
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.method" }}'>
</a-form-item> <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
<a-form-item> </a-form-item>
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }} <a-form-item>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button> <template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
</template> <a-button size="small" @click="inbound.stream.tcp.request.addPath('/')">+</a-button>
<template v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)" v-if="inbound.stream.tcp.request.path.length>1"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
<!-- tcp response -->
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.status" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.statusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
</template> </template>
</a-input> <template v-for="(path, index) in inbound.stream.tcp.request.path">
</a-input-group> <a-input v-model.trim="inbound.stream.tcp.request.path[index]">
</a-form-item> <a-button size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<!-- tcp response -->
<a-divider style="margin:0;">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.status" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.statusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-button size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View file

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

View file

@ -1,183 +1,194 @@
{{define "form/tlsSettings"}} {{define "form/tlsSettings"}}
<!-- tls enable --> <!-- tls enable -->
<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider style="margin:3px 0;"></a-divider> <a-divider style="margin:3px 0;"></a-divider>
<a-form-item label='{{ i18n "security" }}'> <a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid"> <a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button> <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span> <span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template> </template>
<a-radio-button v-if="inbound.canEnableXtls()" value="xtls">XTLS</a-radio-button> <a-radio-button v-if="inbound.canEnableXtls()" value="xtls">XTLS</a-radio-button>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.realityDesc" }}</span> <span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template> </template>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">REALITY</a-radio-button> <a-radio-button v-if="inbound.canEnableReality()" value="reality">REALITY</a-radio-button>
</a-tooltip> </a-tooltip>
<a-radio-button value="tls">TLS</a-radio-button> <a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group>
</a-form-item>
<!-- tls settings -->
<template v-if="inbound.stream.isTls">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item>
<a-form-item label="Cipher Suites">
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">Auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Min/Max Version">
<a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group> </a-radio-group>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px"></a-button> </a-form-item>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px"></a-button>
</a-form-item> <!-- tls settings -->
<template v-if="cert.useFile"> <template v-if="inbound.stream.isTls">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'> <a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="cert.certFile"></a-input> <a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item>
<a-form-item label="Cipher Suites">
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="">Auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Min/Max Version">
<a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label="Reject Unknown SNI">
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"
style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="cert.certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n
"pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
<a-form-item label='OCSP stapling'>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</template>
</template>
<!-- xtls settings -->
<template v-else-if="inbound.xtls">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.xtls.sni"></a-input>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.xtls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.xtls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()"
style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small"
@click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="cert.certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n
"pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</template>
<!-- reality settings -->
<template v-if="inbound.stream.isReality">
<a-form-item label='Show'>
<a-switch v-model="inbound.stream.reality.show"></a-switch>
</a-form-item>
<a-form-item label='Xver'>
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
</a-form-item>
<a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
Short ID
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
</a-icon>
</template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</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>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'> <a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="cert.keyFile"></a-input> <a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
</a-form-item> </a-form-item>
<a-form-item label=" "> <a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> <a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
</a-form-item> </a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
<a-form-item label='OCSP stapling'>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
</template> </template>
</template>
<!-- xtls settings -->
<template v-else-if="inbound.xtls">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.xtls.sni"></a-input>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.xtls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Allow Insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<template v-for="cert,index in inbound.stream.xtls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px"></a-button>
<a-button icon="minus" v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px"></a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="cert.certFile"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</template>
<!-- reality settings -->
<template v-if="inbound.stream.isReality">
<a-form-item label='Show'>
<a-switch v-model="inbound.stream.reality.show"></a-switch>
</a-form-item>
<a-form-item label='Xver'>
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='uTLS'>
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
</a-form-item>
<a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> Short ID <a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
</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='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
</a-form-item>
</template>
</a-form> </a-form>
{{end}} {{end}}

View file

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

View file

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

View file

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

View file

@ -2,23 +2,20 @@
<html lang="en"> <html lang="en">
{{template "head" .}} {{template "head" .}}
<style> <style>
@media (min-width: 769px) { @media (min-width: 769px) {
.ant-layout-content { .ant-layout-content {
margin: 24px 16px; margin: 24px 16px;
}
.ant-card-hoverable {
margin-inline: 0.3rem;
}
} }
.ant-card-hoverable { .ant-col-sm-24 {
margin-inline: 0.3rem; margin-top: 10px;
} }
.ant-alert-error { .ant-card-dark h2 {
margin-inline: 0.3rem; color: var(--dark-color-text-primary);
} }
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-card-dark h2 {
color: var(--dark-color-text-primary);
}
</style> </style>
<body> <body>
@ -88,7 +85,8 @@
<a-card hoverable> <a-card hoverable>
<b>3X-UI:</b> <b>3X-UI:</b>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a> <a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
<a rel="noopener" href="https://t.me/XrayUI" target="_blank"><a-tag color="green">@XrayUI</a-tag></a> <a rel="noopener" href="https://t.me/Kensimon_xee" target="_blank"><a-tag color="green">TG 群组</a-tag></a>
<a rel="noopener" href="https://t.me/i_gaoqian" target="_blank"><a-tag color="green">TG 频道</a-tag></a>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :lg="12">

View file

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
{{template "head" .}} {{template "head" .}}
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
<script src="{{ .base_path }}assets/base64/base64.min.js"></script> <script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/codemirror/codemirror.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script> <script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script> <script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script> <script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
@ -19,44 +19,44 @@
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script> <script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script> <script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
<style> <style>
@media (min-width: 769px) { @media (min-width: 769px) {
.ant-layout-content { .ant-layout-content {
margin: 24px 16px; margin: 24px 16px;
}
} }
} @media (max-width: 768px) {
@media (max-width: 768px) { .ant-tabs-nav .ant-tabs-tab {
.ant-tabs-nav .ant-tabs-tab { margin: 0;
margin: 0; padding: 12px .5rem;
padding: 12px .5rem; }
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
padding: 10px 0px;
}
} }
.ant-table-thead>tr>th, .ant-tabs-bar {
.ant-table-tbody>tr>td { margin: 0;
padding: 10px 0px; }
.ant-list-item {
display: block;
}
.collapse-title {
color: inherit;
font-weight: bold;
font-size: 18px;
padding: 10px 20px;
border-bottom: 2px solid;
}
.collapse-title > i {
color: inherit;
font-size: 24px;
}
.ant-collapse-content-box > li {
padding: 12px 0 0 0 !important;
}
.ant-list-item > li {
padding: 10px 20px !important;
} }
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
.collapse-title {
color: inherit;
font-weight: bold;
font-size: 18px;
padding: 10px 20px;
border-bottom: 2px solid;
}
.collapse-title>i {
color: inherit;
font-size: 24px;
}
.ant-collapse-content-box>li {
padding: 12px 0 0 0 !important;
}
.ant-list-item>li {
padding: 10px 20px !important;
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
@ -443,7 +443,7 @@
<a-table :columns="outboundColumns" bordered <a-table :columns="outboundColumns" bordered
:row-key="r => r.key" :row-key="r => r.key"
:data-source="outboundData" :data-source="outboundData"
:scroll="isMobile ? {} : { x: 800 }" :scroll="isMobile ? {} : { x: 200 }"
:pagination="false" :pagination="false"
:indent-size="0" :indent-size="0"
:style="isMobile ? 'padding: 5px 5px' : 'margin-right: 1px;'"> :style="isMobile ? 'padding: 5px 5px' : 'margin-right: 1px;'">

View file

@ -1,246 +1,261 @@
{{define "ruleModal"}} {{define "ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme"> <a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false"
<a-form-item label='Domain Matcher'> :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option> <a-form-item label='Domain Matcher'>
</a-select> <a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
</a-form-item> <a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
<a-form-item> </a-select>
<template slot="label"> </a-form-item>
<a-tooltip> <a-form-item>
<template slot="title"> <template slot="label">
<span>{{ i18n "pages.xray.rules.useComma" }}</span> <a-tooltip>
</template> Source IPs <a-icon type="question-circle"></a-icon> <template slot="title">
</a-tooltip> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
</template> </template>
<a-input v-model.trim="ruleModal.rule.source"></a-input> Source IPs <a-icon type="question-circle"></a-icon>
</a-form-item> </a-tooltip>
<a-form-item> </template>
<template slot="label"> <a-input v-model.trim="ruleModal.rule.source"></a-input>
<a-tooltip> </a-form-item>
<template slot="title"> <a-form-item>
<span>{{ i18n "pages.xray.rules.useComma" }}</span> <template slot="label">
</template> Source Port <a-icon type="question-circle"></a-icon> <a-tooltip>
</a-tooltip> <template slot="title">
</template> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input> </template>
</a-form-item> Source Port <a-icon type="question-circle"></a-icon>
<a-form-item label='Network'> </a-tooltip>
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme"> </template>
<a-select-option v-for="x in ['','TCP','UDP','TCP,UDP']" :value="x">[[ x ]]</a-select-option> <a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
</a-select> </a-form-item>
</a-form-item> <a-form-item label='Network'>
<a-form-item label='Protocol'> <a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="ruleModal.rule.protocol" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select-option v-for="x in ['','TCP','UDP','TCP,UDP']" :value="x">[[ x ]]</a-select-option>
<a-select-option v-for="x in ['http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option> </a-select>
</a-select> </a-form-item>
</a-form-item> <a-form-item label='Protocol'>
<a-form-item label='Attributes'> <a-select v-model="ruleModal.rule.protocol" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
<a-button icon="plus" size="small" style="margin-left: 10px" @click="ruleModal.rule.attrs.push(['', ''])"></a-button> <a-select-option v-for="x in ['http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option>
</a-form-item> </a-select>
<a-form-item :wrapper-col="{span: 24}"> </a-form-item>
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs"> <a-form-item label='Attributes'>
<a-input style="width: 50%" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'> <a-button size="small" style="margin-left: 10px" @click="ruleModal.rule.attrs.push(['', ''])">+</a-button>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template> </a-form-item>
</a-input> <a-form-item :wrapper-col="{span: 24}">
<a-input style="width: 50%" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'> <a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
<a-button icon="minus" slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)"></a-button> <a-input style="width: 50%" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
</a-input> <template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input-group> </a-input>
</a-form-item> <a-input style="width: 50%" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-form-item> <a-button slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)">-</a-button>
<template slot="label"> </a-input>
<a-tooltip> </a-input-group>
<template slot="title"> </a-form-item>
<span>{{ i18n "pages.xray.rules.useComma" }}</span> <a-form-item>
</template> IP <a-icon type="question-circle"></a-icon> <template slot="label">
</a-tooltip> <a-tooltip>
</template> <template slot="title">
<a-input v-model.trim="ruleModal.rule.ip"></a-input> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
</a-form-item> </template>
<a-form-item> IP <a-icon type="question-circle"></a-icon>
<template slot="label"> </a-tooltip>
<a-tooltip> </template>
<template slot="title"> <a-input v-model.trim="ruleModal.rule.ip"></a-input>
<span>{{ i18n "pages.xray.rules.useComma" }}</span> </a-form-item>
</template> Domain <a-icon type="question-circle"></a-icon> <a-form-item>
</a-tooltip> <template slot="label">
</template> <a-tooltip>
<a-input v-model.trim="ruleModal.rule.domain"></a-input> <template slot="title">
</a-form-item> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
<a-form-item> </template>
<template slot="label"> Domain <a-icon type="question-circle"></a-icon>
<a-tooltip> </a-tooltip>
<template slot="title"> </template>
<span>{{ i18n "pages.xray.rules.useComma" }}</span> <a-input v-model.trim="ruleModal.rule.domain"></a-input>
</template> User <a-icon type="question-circle"></a-icon> </a-form-item>
</a-tooltip> <a-form-item>
</template> <template slot="label">
<a-input v-model.trim="ruleModal.rule.user"></a-input> <a-tooltip>
</a-form-item> <template slot="title">
<a-form-item> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
<template slot="label"> </template>
<a-tooltip> User <a-icon type="question-circle"></a-icon>
<template slot="title"> </a-tooltip>
<span>{{ i18n "pages.xray.rules.useComma" }}</span> </template>
</template> Port <a-icon type="question-circle"></a-icon> <a-input v-model.trim="ruleModal.rule.user"></a-input>
</a-tooltip> </a-form-item>
</template> <a-form-item>
<a-input v-model.trim="ruleModal.rule.port"></a-input> <template slot="label">
</a-form-item> <a-tooltip>
<a-form-item label='Inbound Tags'> <template slot="title">
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
<a-select-option v-for="tag in ruleModal.inboundTags" :value="tag">[[ tag ]]</a-select-option> </template>
</a-select> Port <a-icon type="question-circle"></a-icon>
</a-form-item> </a-tooltip>
<a-form-item label='Outbound Tag'> </template>
<a-select v-model="ruleModal.rule.outboundTag" :dropdown-class-name="themeSwitcher.currentTheme"> <a-input v-model.trim="ruleModal.rule.port"></a-input>
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option> </a-form-item>
</a-select> <a-form-item label='Inbound Tags'>
</a-form-item> <a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
<a-form-item> <a-select-option v-for="tag in ruleModal.inboundTags" :value="tag">[[ tag ]]</a-select-option>
<template slot="label"> </a-select>
<a-tooltip> </a-form-item>
<template slot="title"> <a-form-item label='Outbound Tag'>
<span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span> <a-select v-model="ruleModal.rule.outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
</template> Balancer Tag <a-icon type="question-circle"></a-icon> <a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-tooltip> </a-select>
</template> </a-form-item>
<a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme"> <a-form-item>
<a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option> <template slot="label">
</a-select> <a-tooltip>
</a-form-item> <template slot="title">
</a-form> <span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
</template>
Balancer Tag <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</table>
</a-form>
</a-modal> </a-modal>
<script> <script>
const ruleModal = {
title: '', const ruleModal = {
visible: false, title: '',
confirmLoading: false, visible: false,
okText: '{{ i18n "sure" }}', confirmLoading: false,
isEdit: false, okText: '{{ i18n "sure" }}',
confirm: null, isEdit: false,
rule: { confirm: null,
type: "field", rule: {
domainMatcher: "", type: "field",
domain: "", domainMatcher: "",
ip: "", domain: "",
port: "", ip: "",
sourcePort: "", port: "",
network: "", sourcePort: "",
source: "", network: "",
user: "", source: "",
inboundTag: [], user: "",
protocol: [], inboundTag: [],
attrs: [], protocol: [],
outboundTag: "", attrs: [],
balancerTag: "", outboundTag: "",
}, balancerTag: "",
inboundTags: [], },
outboundTags: [], inboundTags: [],
users: [], outboundTags: [],
balancerTags: [], users: [],
ok() { balancerTags: [],
newRule = ruleModal.getResult(); ok() {
ObjectUtil.execute(ruleModal.confirm, newRule); newRule = ruleModal.getResult();
}, ObjectUtil.execute(ruleModal.confirm, newRule);
show({ },
title = '', show({ title='', okText='{{ i18n "sure" }}', rule, confirm=(rule)=>{}, isEdit=false }) {
okText = '{{ i18n "sure" }}', this.title = title;
rule, this.okText = okText;
confirm = (rule) => {}, this.confirm = confirm;
isEdit = false this.visible = true;
}) { if(isEdit) {
this.title = title; this.rule.domainMatcher = rule.domainMatcher;
this.okText = okText; this.rule.domain = rule.domain ? rule.domain.join(',') : [];
this.confirm = confirm; this.rule.ip = rule.ip ? rule.ip.join(',') : [];
this.visible = true; this.rule.port = rule.port;
if (isEdit) { this.rule.sourcePort = rule.sourcePort;
this.rule.domainMatcher = rule.domainMatcher; this.rule.network = rule.network;
this.rule.domain = rule.domain ? rule.domain.join(',') : []; this.rule.source = rule.source ? rule.source.join(',') : [];
this.rule.ip = rule.ip ? rule.ip.join(',') : []; this.rule.user = rule.user ? rule.user.join(',') : [];
this.rule.port = rule.port; this.rule.inboundTag = rule.inboundTag;
this.rule.sourcePort = rule.sourcePort; this.rule.protocol = rule.protocol;
this.rule.network = rule.network; this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
this.rule.source = rule.source ? rule.source.join(',') : []; this.rule.outboundTag = rule.outboundTag;
this.rule.user = rule.user ? rule.user.join(',') : []; this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
this.rule.inboundTag = rule.inboundTag; } else {
this.rule.protocol = rule.protocol; this.rule = {
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : []; domainMatcher: "",
this.rule.outboundTag = rule.outboundTag; domain: "",
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : ""; ip: "",
} else { port: "",
this.rule = { sourcePort: "",
domainMatcher: "", network: "",
domain: "", source: "",
ip: "", user: "",
port: "", inboundTag: [],
sourcePort: "", protocol: [],
network: "", attrs: [],
source: "", outboundTag: "",
user: "", balancerTag: "",
inboundTag: [], }
protocol: [], }
attrs: [], this.isEdit = isEdit;
outboundTag: "", this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
balancerTag: "", this.inboundTags.push(...app.inboundTags);
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
if(app.templateSettings.reverse){
if(app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = [ "", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
}
},
close() {
ruleModal.visible = false;
ruleModal.loading(false);
},
loading(loading=true) {
ruleModal.confirmLoading = loading;
},
getResult() {
value = ruleModal.rule;
rule = {};
newRule = {};
rule.type = "field";
rule.domainMatcher = value.domainMatcher;
rule.domain = value.domain.length>0 ? value.domain.split(',') : [];
rule.ip = value.ip.length>0 ? value.ip.split(',') : [];
rule.port = value.port;
rule.sourcePort = value.sourcePort;
rule.network = value.network;
rule.source = value.source.length>0 ? value.source.split(',') : [];
rule.user = value.user.length>0 ? value.user.split(',') : [];
rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (
value !== null &&
value !== undefined &&
!(Array.isArray(value) && value.length === 0) &&
!(typeof value === 'object' && Object.keys(value).length === 0) &&
value !== ''
) {
newRule[key] = value;
}
}
return newRule;
} }
} };
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag); new Vue({
this.inboundTags.push(...app.inboundTags); delimiters: ['[[', ']]'],
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag) el: '#rule-modal',
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)]; data: {
if (app.templateSettings.reverse) { ruleModal: ruleModal,
if (app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
} }
if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag)); });
}
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
}
},
close() {
ruleModal.visible = false;
ruleModal.loading(false);
},
loading(loading = true) {
ruleModal.confirmLoading = loading;
},
getResult() {
value = ruleModal.rule;
rule = {};
newRule = {};
rule.type = "field";
rule.domainMatcher = value.domainMatcher;
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
rule.port = value.port;
rule.sourcePort = value.sourcePort;
rule.network = value.network;
rule.source = value.source.length > 0 ? value.source.split(',') : [];
rule.user = value.user.length > 0 ? value.user.split(',') : [];
rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
newRule[key] = value;
}
}
return newRule;
}
};
new Vue({
delimiters: ['[[', ']]'],
el: '#rule-modal',
data: {
ruleModal: ruleModal,
}
});
</script> </script>
{{end}} {{end}}

View file

@ -609,7 +609,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
oldEmail := "" oldEmail := ""
newClientId := "" newClientId := ""
clientIndex := -1 clientIndex := 0
for index, oldClient := range oldClients { for index, oldClient := range oldClients {
oldClientId := "" oldClientId := ""
if oldInbound.Protocol == "trojan" { if oldInbound.Protocol == "trojan" {
@ -630,7 +630,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
} }
// Validate new client ID // Validate new client ID
if newClientId == "" || clientIndex == -1 { if newClientId == "" {
return false, common.NewError("empty client ID") return false, common.NewError("empty client ID")
} }

View file

@ -37,7 +37,7 @@ var defaultValueMap = map[string]string{
"expireDiff": "0", "expireDiff": "0",
"trafficDiff": "0", "trafficDiff": "0",
"remarkModel": "-ieo", "remarkModel": "-ieo",
"timeLocation": "Asia/Tehran", "timeLocation": "Asia/Shanghai",
"tgBotEnable": "false", "tgBotEnable": "false",
"tgBotToken": "", "tgBotToken": "",
"tgBotProxy": "", "tgBotProxy": "",

View file

@ -5,7 +5,7 @@ import (
"x-ui/database/model" "x-ui/database/model"
"github.com/gin-contrib/sessions" sessions "github.com/Calidity/gin-sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )

View file

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

View file

@ -24,9 +24,9 @@ import (
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
sessions "github.com/Calidity/gin-sessions"
"github.com/Calidity/gin-sessions/cookie"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )

525
x-ui.sh

File diff suppressed because it is too large Load diff