mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-09 03:06:37 +08:00
修复ui 添加gost静默http服务
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
- 可针对 **指定用户的指定隧道进行限速** 设置
|
- 可针对 **指定用户的指定隧道进行限速** 设置
|
||||||
- 支持配置 **单向或双向流量计费方式**,灵活适配不同计费模型
|
- 支持配置 **单向或双向流量计费方式**,灵活适配不同计费模型
|
||||||
- 提供灵活的转发策略配置,适用于多种网络代理场景
|
- 提供灵活的转发策略配置,适用于多种网络代理场景
|
||||||
|
- 内置轻量级 Agent 端 API 服务,默认强制启用自签发 SSL,仅响应已认证请求,静默运行、安全可控
|
||||||
|
|
||||||
---
|
---
|
||||||
# 流量统计算法
|
# 流量统计算法
|
||||||
@@ -72,6 +73,9 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
|
ipv6需要面板端支持,同时开启docker的ipv6服务和composer中的ipv6
|
||||||
|
|
||||||
|
|
||||||
如果以前安装过需要重新安装
|
如果以前安装过需要重新安装
|
||||||
推荐先删除本地上次下载的文件
|
推荐先删除本地上次下载的文件
|
||||||
panel_install.sh
|
panel_install.sh
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func Register(r *gin.Engine, opts *Options) {
|
|||||||
AllowPrivateNetwork: true,
|
AllowPrivateNetwork: true,
|
||||||
})),
|
})),
|
||||||
gin.Recovery(),
|
gin.Recovery(),
|
||||||
|
GlobalInterceptor(),
|
||||||
)
|
)
|
||||||
if opts.AccessLog {
|
if opts.AccessLog {
|
||||||
r.Use(mwLogger())
|
r.Use(mwLogger())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -45,3 +46,48 @@ func mwBasicAuth(auther auth.Authenticator) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GlobalInterceptor 全局HTTP请求拦截器
|
||||||
|
func GlobalInterceptor() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 检查认证参数,如果没有认证就静默关闭
|
||||||
|
if !hasValidAuth(c) {
|
||||||
|
// 获取底层连接并直接关闭
|
||||||
|
if hijacker, ok := c.Writer.(http.Hijacker); ok {
|
||||||
|
if conn, _, err := hijacker.Hijack(); err == nil {
|
||||||
|
conn.Close() // 直接关闭连接,不发送任何数据
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果无法hijack连接,则中止请求但不返回响应
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasValidAuth 检查请求是否包含有效的认证信息
|
||||||
|
func hasValidAuth(c *gin.Context) bool {
|
||||||
|
// 获取Authorization头
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是Basic认证
|
||||||
|
if !strings.HasPrefix(authHeader, "Basic ") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析Basic认证
|
||||||
|
_, _, ok := c.Request.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (l *tlsListener) Init(md md.Metadata) (err error) {
|
|||||||
ln = limiter_wrapper.WrapListener(l.options.Service, ln, l.options.TrafficLimiter)
|
ln = limiter_wrapper.WrapListener(l.options.Service, ln, l.options.TrafficLimiter)
|
||||||
ln = climiter.WrapListener(l.options.ConnLimiter, ln)
|
ln = climiter.WrapListener(l.options.ConnLimiter, ln)
|
||||||
|
|
||||||
l.ln = tls.NewListener(ln, l.options.TLSConfig)
|
l.ln = newSilentTLSListener(ln, l.options.TLSConfig, l.logger)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -99,3 +99,45 @@ func (l *tlsListener) Addr() net.Addr {
|
|||||||
func (l *tlsListener) Close() error {
|
func (l *tlsListener) Close() error {
|
||||||
return l.ln.Close()
|
return l.ln.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// silentTLSListener 是一个自定义的TLS监听器,对非TLS连接静默处理
|
||||||
|
type silentTLSListener struct {
|
||||||
|
net.Listener
|
||||||
|
config *tls.Config
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSilentTLSListener(inner net.Listener, config *tls.Config, logger logger.Logger) net.Listener {
|
||||||
|
return &silentTLSListener{
|
||||||
|
Listener: inner,
|
||||||
|
config: config,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *silentTLSListener) Accept() (net.Conn, error) {
|
||||||
|
for {
|
||||||
|
conn, err := l.Listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建TLS连接
|
||||||
|
tlsConn := tls.Server(conn, l.config)
|
||||||
|
|
||||||
|
// 设置握手超时
|
||||||
|
tlsConn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
|
||||||
|
// 尝试TLS握手
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
continue // 继续接受下一个连接
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除超时设置
|
||||||
|
tlsConn.SetDeadline(time.Time{})
|
||||||
|
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+53
-31
@@ -5,15 +5,17 @@ if [[ "$ARCH" != "x86_64" ]]; then
|
|||||||
echo "❌ 不支持的架构: $ARCH,仅支持 x86_64。"
|
echo "❌ 不支持的架构: $ARCH,仅支持 x86_64。"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# gost下载地址
|
|
||||||
|
# 下载地址
|
||||||
DOWNLOAD_URL="https://github.com/bqlpfy/forward-panel/releases/download/gost/gost"
|
DOWNLOAD_URL="https://github.com/bqlpfy/forward-panel/releases/download/gost/gost"
|
||||||
|
|
||||||
|
# 解析参数
|
||||||
while getopts "a:p:s:" opt; do
|
while getopts "a:p:s:" opt; do
|
||||||
case $opt in
|
case $opt in
|
||||||
a) SERVER_ADDR="$OPTARG" ;;
|
a) SERVER_ADDR="$OPTARG" ;;
|
||||||
p) PORT="$OPTARG" ;;
|
p) PORT="$OPTARG" ;;
|
||||||
s) SECRET="$OPTARG" ;;
|
s) SECRET="$OPTARG" ;;
|
||||||
*) echo "无效参数"; exit 1 ;;
|
*) echo "❌ 无效参数"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -22,46 +24,60 @@ if [[ -z "$SERVER_ADDR" || -z "$PORT" || -z "$SECRET" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
INSTALL_DIR="/etc/gost"
|
INSTALL_DIR="/etc/gost"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
# 检查gost服务是否存在并运行
|
# 停止并禁用已有服务
|
||||||
if systemctl list-units --full -all | grep -Fq "gost.service"; then
|
if systemctl list-units --full -all | grep -Fq "gost.service"; then
|
||||||
echo "检测到已存在的gost服务"
|
echo "🔍 检测到已存在的gost服务"
|
||||||
if systemctl is-active --quiet gost; then
|
systemctl stop gost 2>/dev/null && echo "🛑 停止服务"
|
||||||
echo "停止运行中的gost服务..."
|
systemctl disable gost 2>/dev/null && echo "🚫 禁用自启"
|
||||||
systemctl stop gost
|
|
||||||
fi
|
|
||||||
if systemctl is-enabled --quiet gost; then
|
|
||||||
echo "禁用gost服务自启动..."
|
|
||||||
systemctl disable gost
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 删除旧文件
|
# 删除旧文件
|
||||||
for FILE in gost config.json gost.json; do
|
[[ -f "$INSTALL_DIR/gost" ]] && echo "🧹 删除旧文件 gost" && rm -f "$INSTALL_DIR/gost"
|
||||||
if [[ -f "$INSTALL_DIR/$FILE" ]]; then
|
|
||||||
echo "删除已有文件: $INSTALL_DIR/$FILE"
|
|
||||||
rm -f "$INSTALL_DIR/$FILE"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
|
# 下载 gost
|
||||||
# 下载gost可执行文件
|
echo "⬇️ 下载 gost 中..."
|
||||||
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost"
|
curl -L "$DOWNLOAD_URL" -o "$INSTALL_DIR/gost"
|
||||||
|
if [[ ! -f "$INSTALL_DIR/gost" || ! -s "$INSTALL_DIR/gost" ]]; then
|
||||||
|
echo "❌ 下载失败,请检查网络或下载链接。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
chmod +x "$INSTALL_DIR/gost"
|
chmod +x "$INSTALL_DIR/gost"
|
||||||
|
echo "✅ 下载完成"
|
||||||
|
|
||||||
|
# 打印版本
|
||||||
|
echo "🔎 gost 版本:$($INSTALL_DIR/gost -V)"
|
||||||
|
|
||||||
cat > "$INSTALL_DIR/config.json" <<EOF
|
# 写入 config.json
|
||||||
|
CONFIG_FILE="$INSTALL_DIR/config.json"
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
echo "📝 更新配置: config.json"
|
||||||
|
sed -i.bak "s|\"addr\": \".*\"|\"addr\": \"$SERVER_ADDR\"|g" "$CONFIG_FILE"
|
||||||
|
sed -i.bak "s|\"secret\": \".*\"|\"secret\": \"$SECRET\"|g" "$CONFIG_FILE"
|
||||||
|
rm -f "$CONFIG_FILE.bak"
|
||||||
|
else
|
||||||
|
echo "📄 创建新配置: config.json"
|
||||||
|
cat > "$CONFIG_FILE" <<EOF
|
||||||
{
|
{
|
||||||
"addr": "$SERVER_ADDR",
|
"addr": "$SERVER_ADDR",
|
||||||
"secret": "$SECRET"
|
"secret": "$SECRET"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 写入 gost.json
|
||||||
cat > "$INSTALL_DIR/gost.json" <<EOF
|
GOST_CONFIG="$INSTALL_DIR/gost.json"
|
||||||
|
if [[ -f "$GOST_CONFIG" ]]; then
|
||||||
|
echo "📝 更新配置: gost.json"
|
||||||
|
sed -i.bak "s|\"addr\": \":.*\"|\"addr\": \":$PORT\"|g" "$GOST_CONFIG"
|
||||||
|
sed -i.bak "s|\"username\": \".*\"|\"username\": \"$SECRET\"|g" "$GOST_CONFIG"
|
||||||
|
sed -i.bak "s|\"password\": \".*\"|\"password\": \"$SECRET\"|g" "$GOST_CONFIG"
|
||||||
|
rm -f "$GOST_CONFIG.bak"
|
||||||
|
else
|
||||||
|
echo "📄 创建新配置: gost.json"
|
||||||
|
cat > "$GOST_CONFIG" <<EOF
|
||||||
{
|
{
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
@@ -84,8 +100,14 @@ cat > "$INSTALL_DIR/gost.json" <<EOF
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
cat > /etc/systemd/system/gost.service <<EOF
|
# 加强权限
|
||||||
|
chmod 600 "$INSTALL_DIR"/*.json
|
||||||
|
|
||||||
|
# 创建 systemd 服务
|
||||||
|
SERVICE_FILE="/etc/systemd/system/gost.service"
|
||||||
|
cat > "$SERVICE_FILE" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Gost Proxy Service
|
Description=Gost Proxy Service
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -99,18 +121,18 @@ Restart=on-failure
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
systemctl daemon-reexec
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable gost
|
systemctl enable gost
|
||||||
systemctl start gost
|
systemctl start gost
|
||||||
|
|
||||||
# 检查服务状态
|
# 检查状态
|
||||||
|
echo "🔄 检查服务状态..."
|
||||||
if systemctl is-active --quiet gost; then
|
if systemctl is-active --quiet gost; then
|
||||||
echo "✅ 安装完成,gost服务已启动并设置为开机启动。"
|
echo "✅ 安装完成,gost服务已启动并设置为开机启动。"
|
||||||
echo "配置文件位于: $INSTALL_DIR"
|
echo "📁 配置目录: $INSTALL_DIR"
|
||||||
echo "服务状态: $(systemctl is-active gost)"
|
echo "🔧 服务状态: $(systemctl is-active gost)"
|
||||||
else
|
else
|
||||||
echo "❌ gost服务启动失败,请检查日志:"
|
echo "❌ gost服务启动失败,请执行以下命令查看日志:"
|
||||||
echo "journalctl -u gost -f"
|
echo "journalctl -u gost -f"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ public class TunnelDto {
|
|||||||
private Long outNodeId;
|
private Long outNodeId;
|
||||||
|
|
||||||
// 出口端口开始,当type=1时可以为空,会自动设置为入口端口
|
// 出口端口开始,当type=1时可以为空,会自动设置为入口端口
|
||||||
|
@Min(value = 0, message = "出口端口开始必须大于等于1")
|
||||||
|
@Max(value = 65535, message = "出口端口开始不能超过65535")
|
||||||
private Integer outIpSta;
|
private Integer outIpSta;
|
||||||
|
|
||||||
// 出口端口结束,当type=1时可以为空,会自动设置为入口端口
|
// 出口端口结束,当type=1时可以为空,会自动设置为入口端口
|
||||||
|
@Min(value = 0, message = "出口端口结束必须大于等于1")
|
||||||
|
@Max(value = 65535, message = "出口端口结束不能超过65535")
|
||||||
private Integer outIpEnd;
|
private Integer outIpEnd;
|
||||||
|
|
||||||
@NotNull(message = "隧道类型不能为空")
|
@NotNull(message = "隧道类型不能为空")
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TunnelUpdateDto {
|
||||||
|
|
||||||
|
@NotNull(message = "隧道ID不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "隧道名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@NotNull(message = "流量计算类型不能为空")
|
||||||
|
private Integer flow;
|
||||||
|
|
||||||
|
@NotNull(message = "入口端口开始不能为空")
|
||||||
|
@Min(value = 1, message = "入口端口开始必须大于0")
|
||||||
|
@Max(value = 65535, message = "入口端口开始不能超过65535")
|
||||||
|
private Integer inPortSta;
|
||||||
|
|
||||||
|
@NotNull(message = "入口端口结束不能为空")
|
||||||
|
@Min(value = 1, message = "入口端口结束必须大于0")
|
||||||
|
@Max(value = 65535, message = "入口端口结束不能超过65535")
|
||||||
|
private Integer inPortEnd;
|
||||||
|
|
||||||
|
// 出口端口开始(对于隧道转发类型的隧道)
|
||||||
|
@Min(value = 1, message = "出口端口开始必须大于等于0")
|
||||||
|
@Max(value = 65535, message = "出口端口开始不能超过65535")
|
||||||
|
private Integer outIpSta;
|
||||||
|
|
||||||
|
// 出口端口结束(对于隧道转发类型的隧道)
|
||||||
|
@Min(value = 1, message = "出口端口结束必须大于等于0")
|
||||||
|
@Max(value = 65535, message = "出口端口结束不能超过65535")
|
||||||
|
private Integer outIpEnd;
|
||||||
|
}
|
||||||
@@ -9,8 +9,7 @@ import javax.validation.constraints.Min;
|
|||||||
@Data
|
@Data
|
||||||
public class UserDto {
|
public class UserDto {
|
||||||
|
|
||||||
@NotBlank(message = "姓名不能为空")
|
private String name = "user";
|
||||||
private String name;
|
|
||||||
|
|
||||||
@NotBlank(message = "用户名不能为空")
|
@NotBlank(message = "用户名不能为空")
|
||||||
private String user;
|
private String user;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.admin.common.aop.LogAnnotation;
|
|||||||
import com.admin.common.annotation.RequireRole;
|
import com.admin.common.annotation.RequireRole;
|
||||||
import com.admin.common.dto.PageDto;
|
import com.admin.common.dto.PageDto;
|
||||||
import com.admin.common.dto.TunnelDto;
|
import com.admin.common.dto.TunnelDto;
|
||||||
|
import com.admin.common.dto.TunnelUpdateDto;
|
||||||
|
|
||||||
import com.admin.common.dto.UserTunnelDto;
|
import com.admin.common.dto.UserTunnelDto;
|
||||||
import com.admin.common.dto.UserTunnelQueryDto;
|
import com.admin.common.dto.UserTunnelQueryDto;
|
||||||
@@ -50,7 +51,12 @@ public class TunnelController extends BaseController {
|
|||||||
return tunnelService.getAllTunnels();
|
return tunnelService.getAllTunnels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@LogAnnotation
|
||||||
|
@RequireRole
|
||||||
|
@PostMapping("/update")
|
||||||
|
public R update(@Validated @RequestBody TunnelUpdateDto tunnelUpdateDto) {
|
||||||
|
return tunnelService.updateTunnel(tunnelUpdateDto);
|
||||||
|
}
|
||||||
|
|
||||||
@LogAnnotation
|
@LogAnnotation
|
||||||
@RequireRole
|
@RequireRole
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.admin.service;
|
|||||||
|
|
||||||
import com.admin.common.dto.PageDto;
|
import com.admin.common.dto.PageDto;
|
||||||
import com.admin.common.dto.TunnelDto;
|
import com.admin.common.dto.TunnelDto;
|
||||||
|
import com.admin.common.dto.TunnelUpdateDto;
|
||||||
|
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.entity.Tunnel;
|
import com.admin.entity.Tunnel;
|
||||||
@@ -30,7 +31,12 @@ public interface TunnelService extends IService<Tunnel> {
|
|||||||
*/
|
*/
|
||||||
R getAllTunnels();
|
R getAllTunnels();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新隧道(只允许修改名称、流量计费、端口范围)
|
||||||
|
* @param tunnelUpdateDto 更新数据
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
R updateTunnel(TunnelUpdateDto tunnelUpdateDto);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除隧道
|
* 删除隧道
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.admin.service.impl;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.admin.common.dto.TunnelDto;
|
import com.admin.common.dto.TunnelDto;
|
||||||
import com.admin.common.dto.TunnelListDto;
|
import com.admin.common.dto.TunnelListDto;
|
||||||
|
import com.admin.common.dto.TunnelUpdateDto;
|
||||||
|
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.common.utils.JwtUtil;
|
import com.admin.common.utils.JwtUtil;
|
||||||
@@ -110,22 +111,30 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
return nameValidationResult;
|
return nameValidationResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 验证入口节点和端口
|
// 2. 验证隧道转发类型的必要参数
|
||||||
|
if (tunnelDto.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) {
|
||||||
|
R tunnelForwardValidationResult = validateTunnelForwardCreate(tunnelDto);
|
||||||
|
if (tunnelForwardValidationResult.getCode() != 0) {
|
||||||
|
return tunnelForwardValidationResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证入口节点和端口
|
||||||
NodeValidationResult inNodeValidation = validateInNode(tunnelDto);
|
NodeValidationResult inNodeValidation = validateInNode(tunnelDto);
|
||||||
if (inNodeValidation.isHasError()) {
|
if (inNodeValidation.isHasError()) {
|
||||||
return R.err(inNodeValidation.getErrorMessage());
|
return R.err(inNodeValidation.getErrorMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 构建隧道实体
|
// 4. 构建隧道实体
|
||||||
Tunnel tunnel = buildTunnelEntity(tunnelDto, inNodeValidation.getNode());
|
Tunnel tunnel = buildTunnelEntity(tunnelDto, inNodeValidation.getNode());
|
||||||
|
|
||||||
// 4. 根据隧道类型设置出口参数
|
// 5. 根据隧道类型设置出口参数
|
||||||
R outNodeSetupResult = setupOutNodeParameters(tunnel, tunnelDto);
|
R outNodeSetupResult = setupOutNodeParameters(tunnel, tunnelDto);
|
||||||
if (outNodeSetupResult.getCode() != 0) {
|
if (outNodeSetupResult.getCode() != 0) {
|
||||||
return outNodeSetupResult;
|
return outNodeSetupResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 设置默认属性并保存
|
// 6. 设置默认属性并保存
|
||||||
setDefaultTunnelProperties(tunnel);
|
setDefaultTunnelProperties(tunnel);
|
||||||
boolean result = this.save(tunnel);
|
boolean result = this.save(tunnel);
|
||||||
|
|
||||||
@@ -143,6 +152,60 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
return R.ok(tunnelList);
|
return R.ok(tunnelList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新隧道(只允许修改名称、流量计费、端口范围)
|
||||||
|
*
|
||||||
|
* @param tunnelUpdateDto 更新数据传输对象
|
||||||
|
* @return 更新结果响应
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public R updateTunnel(TunnelUpdateDto tunnelUpdateDto) {
|
||||||
|
// 1. 验证隧道是否存在
|
||||||
|
Tunnel existingTunnel = this.getById(tunnelUpdateDto.getId());
|
||||||
|
if (existingTunnel == null) {
|
||||||
|
return R.err(ERROR_TUNNEL_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证隧道名称唯一性(排除自身)
|
||||||
|
R nameValidationResult = validateTunnelNameUniquenessForUpdate(
|
||||||
|
tunnelUpdateDto.getName(), tunnelUpdateDto.getId());
|
||||||
|
if (nameValidationResult.getCode() != 0) {
|
||||||
|
return nameValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证端口范围
|
||||||
|
R portValidationResult = validatePortRanges(tunnelUpdateDto);
|
||||||
|
if (portValidationResult.getCode() != 0) {
|
||||||
|
return portValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证隧道转发类型的出口端口
|
||||||
|
if (existingTunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) {
|
||||||
|
R tunnelTypeValidationResult = validateTunnelForwardUpdate(tunnelUpdateDto);
|
||||||
|
if (tunnelTypeValidationResult.getCode() != 0) {
|
||||||
|
return tunnelTypeValidationResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新允许修改的字段
|
||||||
|
existingTunnel.setName(tunnelUpdateDto.getName());
|
||||||
|
existingTunnel.setFlow(tunnelUpdateDto.getFlow());
|
||||||
|
existingTunnel.setInPortSta(tunnelUpdateDto.getInPortSta());
|
||||||
|
existingTunnel.setInPortEnd(tunnelUpdateDto.getInPortEnd());
|
||||||
|
|
||||||
|
if (existingTunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) {
|
||||||
|
existingTunnel.setOutIpSta(tunnelUpdateDto.getOutIpSta());
|
||||||
|
existingTunnel.setOutIpEnd(tunnelUpdateDto.getOutIpEnd());
|
||||||
|
}else{
|
||||||
|
existingTunnel.setOutIpSta(tunnelUpdateDto.getInPortSta());
|
||||||
|
existingTunnel.setOutIpEnd(tunnelUpdateDto.getInPortEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 保存更新
|
||||||
|
boolean result = this.updateById(existingTunnel);
|
||||||
|
return result ? R.ok("隧道更新成功") : R.err("隧道更新失败");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除隧道
|
* 删除隧道
|
||||||
* 删除前会检查是否有转发或用户权限在使用该隧道
|
* 删除前会检查是否有转发或用户权限在使用该隧道
|
||||||
@@ -213,6 +276,91 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证隧道名称唯一性(更新时使用,排除自身)
|
||||||
|
*
|
||||||
|
* @param tunnelName 隧道名称
|
||||||
|
* @param tunnelId 隧道ID(要排除的隧道)
|
||||||
|
* @return 验证结果响应
|
||||||
|
*/
|
||||||
|
private R validateTunnelNameUniquenessForUpdate(String tunnelName, Long tunnelId) {
|
||||||
|
QueryWrapper<Tunnel> query = new QueryWrapper<>();
|
||||||
|
query.eq("name", tunnelName);
|
||||||
|
query.ne("id", tunnelId); // 排除自身
|
||||||
|
Tunnel existTunnel = this.getOne(query);
|
||||||
|
if (existTunnel != null) {
|
||||||
|
return R.err(ERROR_TUNNEL_NAME_EXISTS);
|
||||||
|
}
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证端口范围
|
||||||
|
*
|
||||||
|
* @param tunnelUpdateDto 更新数据传输对象
|
||||||
|
* @return 验证结果响应
|
||||||
|
*/
|
||||||
|
private R validatePortRanges(TunnelUpdateDto tunnelUpdateDto) {
|
||||||
|
// 验证入口端口范围
|
||||||
|
if (tunnelUpdateDto.getInPortSta() > tunnelUpdateDto.getInPortEnd()) {
|
||||||
|
return R.err(ERROR_IN_PORT_RANGE_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证出口端口范围(如果有的话)
|
||||||
|
if (tunnelUpdateDto.getOutIpSta() != null && tunnelUpdateDto.getOutIpEnd() != null) {
|
||||||
|
if (tunnelUpdateDto.getOutIpSta() > tunnelUpdateDto.getOutIpEnd()) {
|
||||||
|
return R.err(ERROR_OUT_PORT_RANGE_INVALID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证隧道转发更新时的出口端口
|
||||||
|
*
|
||||||
|
* @param tunnelUpdateDto 更新数据传输对象
|
||||||
|
* @return 验证结果响应
|
||||||
|
*/
|
||||||
|
private R validateTunnelForwardUpdate(TunnelUpdateDto tunnelUpdateDto) {
|
||||||
|
// 验证出口端口参数不能为空
|
||||||
|
if (tunnelUpdateDto.getOutIpSta() == null || tunnelUpdateDto.getOutIpEnd() == null) {
|
||||||
|
return R.err(ERROR_OUT_PORT_REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证出口端口范围
|
||||||
|
if (tunnelUpdateDto.getOutIpSta() > tunnelUpdateDto.getOutIpEnd()) {
|
||||||
|
return R.err(ERROR_OUT_PORT_RANGE_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证隧道转发创建时的必要参数
|
||||||
|
*
|
||||||
|
* @param tunnelDto 隧道创建数据传输对象
|
||||||
|
* @return 验证结果响应
|
||||||
|
*/
|
||||||
|
private R validateTunnelForwardCreate(TunnelDto tunnelDto) {
|
||||||
|
// 验证出口节点不能为空
|
||||||
|
if (tunnelDto.getOutNodeId() == null) {
|
||||||
|
return R.err(ERROR_OUT_NODE_REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证出口端口参数不能为空
|
||||||
|
if (tunnelDto.getOutIpSta() == null || tunnelDto.getOutIpEnd() == null) {
|
||||||
|
return R.err(ERROR_OUT_PORT_REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证出口端口范围
|
||||||
|
if (tunnelDto.getOutIpSta() > tunnelDto.getOutIpEnd()) {
|
||||||
|
return R.err(ERROR_OUT_PORT_RANGE_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证入口节点和端口
|
* 验证入口节点和端口
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
|
|
||||||
return R.ok(MapUtil.builder()
|
return R.ok(MapUtil.builder()
|
||||||
.put(LOGIN_TOKEN_FIELD, token)
|
.put(LOGIN_TOKEN_FIELD, token)
|
||||||
.put(LOGIN_NAME_FIELD, user.getName())
|
.put(LOGIN_NAME_FIELD, user.getUser())
|
||||||
.put(LOGIN_ROLE_ID_FIELD, user.getRoleId())
|
.put(LOGIN_ROLE_ID_FIELD, user.getRoleId())
|
||||||
.put(LOGIN_REQUIRE_PASSWORD_CHANGE_FIELD, requirePasswordChange)
|
.put(LOGIN_REQUIRE_PASSWORD_CHANGE_FIELD, requirePasswordChange)
|
||||||
.build());
|
.build());
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ html, body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const getNodeInstallCommand = (id) => Network.post("/node/install", { id
|
|||||||
export const createTunnel = (data) => Network.post("/tunnel/create", data)
|
export const createTunnel = (data) => Network.post("/tunnel/create", data)
|
||||||
export const getTunnelList = () => Network.post("/tunnel/list")
|
export const getTunnelList = () => Network.post("/tunnel/list")
|
||||||
export const getTunnelById = (id) => Network.post("/tunnel/get", { id })
|
export const getTunnelById = (id) => Network.post("/tunnel/get", { id })
|
||||||
|
export const updateTunnel = (data) => Network.post("/tunnel/update", data)
|
||||||
export const deleteTunnel = (id) => Network.post("/tunnel/delete", { id })
|
export const deleteTunnel = (id) => Network.post("/tunnel/delete", { id })
|
||||||
|
|
||||||
// 用户隧道权限管理操作 - 全部使用POST请求
|
// 用户隧道权限管理操作 - 全部使用POST请求
|
||||||
|
|||||||
@@ -95,7 +95,6 @@
|
|||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<p class="warning-text">注意:修改账号密码后需要重新登录</p>
|
<p class="warning-text">注意:修改账号密码后需要重新登录</p>
|
||||||
<p class="copyright">© 2024 管理系统. All rights reserved.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -720,8 +720,6 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.forward-container {
|
.forward-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端优化 */
|
/* 移动端优化 */
|
||||||
|
|||||||
@@ -99,12 +99,8 @@
|
|||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-main :class="['main-content', { 'mobile-main': isMobile }]">
|
<el-main :class="['main-content', { 'mobile-main': isMobile }]">
|
||||||
<div :style="mainContentStyle">
|
<div class="scroll-container">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
|
||||||
<div :class="['footer', { 'mobile-footer': isMobile }]">
|
|
||||||
© 2023 Copyright.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
@@ -358,8 +354,16 @@ export default {
|
|||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: calc(100vh - 100px);
|
|
||||||
transition: margin-left 0.3s ease;
|
transition: margin-left 0.3s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-main {
|
.mobile-main {
|
||||||
@@ -367,13 +371,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 199px;
|
left: 199px;
|
||||||
right: 0;
|
right: 0;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #58666e;
|
color: #58666e;
|
||||||
|
|||||||
@@ -255,6 +255,9 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.loadPackageData();
|
this.loadPackageData();
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
localStorage.setItem('e','/index')
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadPackageData() {
|
async loadPackageData() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -500,8 +503,6 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.package-container {
|
.package-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|||||||
@@ -440,8 +440,6 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.speed-limit-container {
|
.speed-limit-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|||||||
@@ -63,10 +63,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="form-footer">
|
|
||||||
<p class="copyright">© 2024 管理系统. All rights reserved.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+386
-125
@@ -5,12 +5,13 @@
|
|||||||
icon="el-icon-plus"
|
icon="el-icon-plus"
|
||||||
@click="handleAdd"
|
@click="handleAdd"
|
||||||
class="add-btn"
|
class="add-btn"
|
||||||
|
size="medium"
|
||||||
>
|
>
|
||||||
新增隧道
|
新增隧道
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<!-- 隧道卡片展示 -->
|
<!-- 隧道卡片展示 -->
|
||||||
<div class="cards-container" v-loading="loading" style="margin-top: 10px;">
|
<div class="cards-container" v-loading="loading" style="margin-top: 20px;">
|
||||||
<div class="cards-grid">
|
<div class="cards-grid">
|
||||||
<div
|
<div
|
||||||
v-for="tunnel in tunnelList"
|
v-for="tunnel in tunnelList"
|
||||||
@@ -22,29 +23,41 @@
|
|||||||
>
|
>
|
||||||
<!-- 隧道头部 -->
|
<!-- 隧道头部 -->
|
||||||
<div class="tunnel-header">
|
<div class="tunnel-header">
|
||||||
<h3 class="tunnel-name">{{ tunnel.name }}</h3>
|
<div class="tunnel-info">
|
||||||
<div class="tunnel-badges">
|
<h3 class="tunnel-name">{{ tunnel.name }}</h3>
|
||||||
<el-tag v-if="tunnel.type === 1" type="primary" size="mini">端口转发</el-tag>
|
<div class="tunnel-badges">
|
||||||
<el-tag v-else type="warning" size="mini">隧道转发</el-tag>
|
<el-tag v-if="tunnel.type === 1" type="primary" size="mini">端口转发</el-tag>
|
||||||
<el-tag v-if="tunnel.type === 2 && tunnel.protocol"
|
<el-tag v-else type="warning" size="mini">隧道转发</el-tag>
|
||||||
:type="tunnel.protocol === 'tls' ? 'success' : tunnel.protocol === 'mtls' ? 'danger' : 'info'"
|
<el-tag v-if="tunnel.type === 2 && tunnel.protocol"
|
||||||
size="mini">
|
:type="tunnel.protocol === 'tls' ? 'success' : tunnel.protocol === 'mtls' ? 'danger' : 'info'"
|
||||||
{{ tunnel.protocol.toUpperCase() }}
|
size="mini">
|
||||||
</el-tag>
|
{{ tunnel.protocol.toUpperCase() }}
|
||||||
<el-tag v-if="tunnel.flow === 1" type="success" size="mini">单向计算</el-tag>
|
</el-tag>
|
||||||
<el-tag v-else type="info" size="mini">双向计算</el-tag>
|
<el-tag v-if="tunnel.flow === 1" type="success" size="mini">单向计算</el-tag>
|
||||||
|
<el-tag v-else type="info" size="mini">双向计算</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tunnel-actions">
|
||||||
<el-button
|
<el-button
|
||||||
style="margin-left: 10px;"
|
size="small"
|
||||||
size="small"
|
type="primary"
|
||||||
type="danger"
|
icon="el-icon-edit"
|
||||||
icon="el-icon-delete"
|
@click="handleEdit(tunnel)"
|
||||||
@click="handleDelete(tunnel)"
|
class="action-btn edit-btn"
|
||||||
>
|
>
|
||||||
删除
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(tunnel)"
|
||||||
|
class="action-btn delete-btn"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 流程图 -->
|
<!-- 流程图 -->
|
||||||
@@ -171,17 +184,28 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="隧道类型" prop="type">
|
<el-form-item label="隧道类型" prop="type">
|
||||||
<el-radio-group v-model="tunnelForm.type" @change="handleTypeChange">
|
<el-select
|
||||||
<el-radio :label="1">端口转发</el-radio>
|
v-model="tunnelForm.type"
|
||||||
<el-radio :label="2">隧道转发</el-radio>
|
placeholder="请选择隧道类型"
|
||||||
</el-radio-group>
|
style="width: 100%"
|
||||||
|
@change="handleTypeChange"
|
||||||
|
:disabled="isEdit"
|
||||||
|
>
|
||||||
|
<el-option :value="1" label="端口转发"></el-option>
|
||||||
|
<el-option :value="2" label="隧道转发"></el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="流量计算" prop="flow">
|
<el-form-item label="流量计算" prop="flow">
|
||||||
<el-radio-group v-model="tunnelForm.flow">
|
<el-select
|
||||||
<el-radio :label="1">单向计算(仅上传)</el-radio>
|
v-model="tunnelForm.flow"
|
||||||
<el-radio :label="2">双向计算(上传+下载)</el-radio>
|
placeholder="请选择流量计算方式"
|
||||||
</el-radio-group>
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option :value="1" label="单向计算(仅上传)"></el-option>
|
||||||
|
<el-option :value="2" label="双向计算(上传+下载)"></el-option>
|
||||||
|
</el-select>
|
||||||
<div class="form-hint">
|
<div class="form-hint">
|
||||||
单向:仅计算上传流量;双向:计算上传和下载的总流量
|
单向:仅计算上传流量;双向:计算上传和下载的总流量
|
||||||
</div>
|
</div>
|
||||||
@@ -195,6 +219,7 @@
|
|||||||
placeholder="请选择入口节点"
|
placeholder="请选择入口节点"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="handleNodeChange"
|
@change="handleNodeChange"
|
||||||
|
:disabled="isEdit"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="node in onlineNodes"
|
v-for="node in onlineNodes"
|
||||||
@@ -210,45 +235,44 @@
|
|||||||
</span>
|
</span>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="起始端口" prop="inPortSta">
|
||||||
|
<el-input-number
|
||||||
|
v-model="tunnelForm.inPortSta"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
placeholder="起始端口"
|
||||||
|
style="width: 100%"
|
||||||
|
></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="结束端口" prop="inPortEnd">
|
||||||
|
<el-input-number
|
||||||
|
v-model="tunnelForm.inPortEnd"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
placeholder="结束端口"
|
||||||
|
style="width: 100%"
|
||||||
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="起始端口" prop="inPortSta">
|
|
||||||
<el-input-number
|
|
||||||
v-model="tunnelForm.inPortSta"
|
|
||||||
:min="1"
|
|
||||||
:max="65535"
|
|
||||||
placeholder="起始端口"
|
|
||||||
style="width: 100%"
|
|
||||||
></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="结束端口" prop="inPortEnd">
|
|
||||||
<el-input-number
|
|
||||||
v-model="tunnelForm.inPortEnd"
|
|
||||||
:min="1"
|
|
||||||
:max="65535"
|
|
||||||
placeholder="结束端口"
|
|
||||||
style="width: 100%"
|
|
||||||
></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 只有隧道转发(type=2)时才显示出口配置 -->
|
<!-- 只有隧道转发(type=2)时才显示出口配置 -->
|
||||||
<template v-if="tunnelForm.type === 2">
|
<template v-if="tunnelForm.type === 2">
|
||||||
<el-divider content-position="left">出口配置</el-divider>
|
<el-divider content-position="left">出口配置</el-divider>
|
||||||
|
|
||||||
<el-form-item label="协议类型" prop="protocol">
|
<el-form-item label="协议类型" prop="protocol">
|
||||||
<el-radio-group v-model="tunnelForm.protocol">
|
<el-select
|
||||||
<el-radio label="tls">TLS</el-radio>
|
v-model="tunnelForm.protocol"
|
||||||
<el-radio label="tcp">TCP</el-radio>
|
placeholder="请选择协议类型"
|
||||||
<el-radio label="mtls">mTLS</el-radio>
|
style="width: 100%"
|
||||||
</el-radio-group>
|
:disabled="isEdit"
|
||||||
|
>
|
||||||
|
<el-option value="tls" label="TLS"></el-option>
|
||||||
|
<el-option value="tcp" label="TCP"></el-option>
|
||||||
|
<el-option value="mtls" label="mTLS"></el-option>
|
||||||
|
</el-select>
|
||||||
<div class="form-hint">
|
<div class="form-hint">
|
||||||
TLS:传输层安全协议;TCP:传输控制协议;mTLS:双向TLS认证
|
数据过墙推荐选择TLS协议
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -258,6 +282,7 @@
|
|||||||
placeholder="请选择出口节点"
|
placeholder="请选择出口节点"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="handleNodeChange"
|
@change="handleNodeChange"
|
||||||
|
:disabled="isEdit"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="node in onlineNodes"
|
v-for="node in onlineNodes"
|
||||||
@@ -274,32 +299,26 @@
|
|||||||
</span>
|
</span>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-row :gutter="20">
|
</el-form-item>
|
||||||
<el-col :span="12">
|
<el-form-item label="起始端口" prop="outIpSta">
|
||||||
<el-form-item label="起始端口" prop="outIpSta">
|
<el-input-number
|
||||||
<el-input-number
|
v-model="tunnelForm.outIpSta"
|
||||||
v-model="tunnelForm.outIpSta"
|
:min="1"
|
||||||
:min="10000"
|
:max="65535"
|
||||||
:max="65535"
|
placeholder="起始端口"
|
||||||
placeholder="起始端口"
|
style="width: 100%"
|
||||||
style="width: 100%"
|
></el-input-number>
|
||||||
></el-input-number>
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-form-item label="结束端口" prop="outIpEnd">
|
||||||
</el-col>
|
<el-input-number
|
||||||
<el-col :span="12">
|
v-model="tunnelForm.outIpEnd"
|
||||||
<el-form-item label="结束端口" prop="outIpEnd">
|
:min="1"
|
||||||
<el-input-number
|
:max="65535"
|
||||||
v-model="tunnelForm.outIpEnd"
|
placeholder="结束端口"
|
||||||
:min="10000"
|
style="width: 100%"
|
||||||
:max="65535"
|
></el-input-number>
|
||||||
placeholder="结束端口"
|
</el-form-item>
|
||||||
style="width: 100%"
|
|
||||||
></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -313,13 +332,19 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
<el-button
|
||||||
|
@click="dialogVisible = false"
|
||||||
|
class="dialog-btn cancel-btn"
|
||||||
|
>
|
||||||
|
取 消
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
:loading="submitLoading"
|
:loading="submitLoading"
|
||||||
|
class="dialog-btn submit-btn"
|
||||||
>
|
>
|
||||||
{{ submitLoading ? '创建中...' : '创 建' }}
|
{{ submitLoading ? (isEdit ? '更新中...' : '创建中...') : (isEdit ? '更 新' : '创 建') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -327,7 +352,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createTunnel, deleteTunnel, getTunnelList, getNodeList } from "@/api";
|
import { createTunnel, updateTunnel, deleteTunnel, getTunnelList, getNodeList } from "@/api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Tunnel",
|
name: "Tunnel",
|
||||||
@@ -339,14 +364,15 @@ export default {
|
|||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: '新增隧道',
|
dialogTitle: '新增隧道',
|
||||||
submitLoading: false,
|
submitLoading: false,
|
||||||
|
isEdit: false,
|
||||||
tunnelForm: {
|
tunnelForm: {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
inNodeId: null,
|
inNodeId: null,
|
||||||
inPortSta: 10000,
|
inPortSta: 1,
|
||||||
inPortEnd: 65535,
|
inPortEnd: 65535,
|
||||||
outNodeId: null,
|
outNodeId: null,
|
||||||
outIpSta: 10000,
|
outIpSta: 1,
|
||||||
outIpEnd: 65535,
|
outIpEnd: 65535,
|
||||||
type: 1,
|
type: 1,
|
||||||
flow: 1, // 默认单向计算
|
flow: 1, // 默认单向计算
|
||||||
@@ -450,11 +476,37 @@ export default {
|
|||||||
// 新增隧道
|
// 新增隧道
|
||||||
handleAdd() {
|
handleAdd() {
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
|
this.isEdit = false;
|
||||||
|
this.dialogTitle = '新增隧道';
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.loadNodes(); // 打开对话框时加载节点列表
|
this.loadNodes(); // 打开对话框时加载节点列表
|
||||||
},
|
},
|
||||||
|
|
||||||
// 编辑功能已移除 - 隧道创建后不能修改
|
// 编辑隧道 - 只能修改名称、流量计费、端口范围
|
||||||
|
handleEdit(tunnel) {
|
||||||
|
this.isEdit = true;
|
||||||
|
this.dialogTitle = '编辑隧道';
|
||||||
|
this.dialogVisible = true;
|
||||||
|
|
||||||
|
// 填充表单数据(只允许修改特定字段)
|
||||||
|
this.tunnelForm = {
|
||||||
|
id: tunnel.id,
|
||||||
|
name: tunnel.name,
|
||||||
|
flow: tunnel.flow,
|
||||||
|
inPortSta: tunnel.inPortSta,
|
||||||
|
inPortEnd: tunnel.inPortEnd,
|
||||||
|
outIpSta: tunnel.outIpSta,
|
||||||
|
outIpEnd: tunnel.outIpEnd,
|
||||||
|
// 以下字段不允许修改,但需要保留原值用于显示和提交
|
||||||
|
type: tunnel.type,
|
||||||
|
inNodeId: tunnel.inNodeId,
|
||||||
|
outNodeId: tunnel.outNodeId,
|
||||||
|
protocol: tunnel.protocol,
|
||||||
|
status: tunnel.status
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadNodes(); // 打开对话框时加载节点列表
|
||||||
|
},
|
||||||
|
|
||||||
// 删除隧道
|
// 删除隧道
|
||||||
handleDelete(tunnel) {
|
handleDelete(tunnel) {
|
||||||
@@ -527,36 +579,65 @@ export default {
|
|||||||
|
|
||||||
this.submitLoading = true;
|
this.submitLoading = true;
|
||||||
|
|
||||||
// 只支持创建隧道,不支持编辑
|
|
||||||
const data = { ...this.tunnelForm };
|
const data = { ...this.tunnelForm };
|
||||||
|
|
||||||
createTunnel(data).then(res => {
|
if (this.isEdit) {
|
||||||
this.submitLoading = false;
|
// 编辑模式 - 只更新允许修改的字段
|
||||||
if (res.code === 0) {
|
const updateData = {
|
||||||
this.$message.success('创建成功');
|
id: data.id,
|
||||||
this.dialogVisible = false;
|
name: data.name,
|
||||||
this.loadTunnels();
|
flow: data.flow,
|
||||||
} else {
|
inPortSta: data.inPortSta,
|
||||||
this.$message.error(res.msg || '创建失败');
|
inPortEnd: data.inPortEnd,
|
||||||
}
|
outIpSta: data.outIpSta,
|
||||||
}).catch(() => {
|
outIpEnd: data.outIpEnd
|
||||||
this.submitLoading = false;
|
};
|
||||||
this.$message.error('网络错误,请重试');
|
|
||||||
});
|
updateTunnel(updateData).then(res => {
|
||||||
|
this.submitLoading = false;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success('更新成功');
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.loadTunnels();
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '更新失败');
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.submitLoading = false;
|
||||||
|
this.$message.error('网络错误,请重试');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 创建模式
|
||||||
|
createTunnel(data).then(res => {
|
||||||
|
this.submitLoading = false;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success('创建成功');
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.loadTunnels();
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '创建失败');
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.submitLoading = false;
|
||||||
|
this.$message.error('网络错误,请重试');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
this.isEdit = false;
|
||||||
|
this.dialogTitle = '新增隧道';
|
||||||
this.tunnelForm = {
|
this.tunnelForm = {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
inNodeId: null,
|
inNodeId: null,
|
||||||
inPortSta: 10000,
|
inPortSta: 1,
|
||||||
inPortEnd: 65535,
|
inPortEnd: 65535,
|
||||||
outNodeId: null,
|
outNodeId: null,
|
||||||
outIpSta: 10000,
|
outIpSta: 1,
|
||||||
outIpEnd: 65535,
|
outIpEnd: 65535,
|
||||||
type: 1,
|
type: 1,
|
||||||
flow: 1, // 默认单向计算
|
flow: 1, // 默认单向计算
|
||||||
@@ -574,8 +655,6 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.tunnel-container {
|
.tunnel-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面头部 */
|
/* 页面头部 */
|
||||||
@@ -800,13 +879,102 @@ export default {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 操作按钮 */
|
/* 隧道信息容器 */
|
||||||
|
.tunnel-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮容器 */
|
||||||
.tunnel-actions {
|
.tunnel-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮样式 */
|
||||||
|
.action-btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
background: #409eff;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
border-color: #66b1ff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background: #f56c6c;
|
||||||
|
border-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #f78989;
|
||||||
|
border-color: #f78989;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(245, 108, 108, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增按钮样式 */
|
||||||
|
.add-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框按钮样式 */
|
||||||
|
.dialog-footer {
|
||||||
|
padding-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
padding-top: 15px;
|
}
|
||||||
margin-top: auto;
|
|
||||||
|
.dialog-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 80px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-color: #dcdfe6;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: #ecf5ff;
|
||||||
|
border-color: #b3d8ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单提示 */
|
/* 表单提示 */
|
||||||
@@ -817,6 +985,12 @@ export default {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 只读字段提示样式 */
|
||||||
|
.form-readonly-hint {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
/* 空状态 */
|
/* 空状态 */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -879,23 +1053,38 @@ export default {
|
|||||||
.tunnel-header {
|
.tunnel-header {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: stretch;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tunnel-info {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.tunnel-name {
|
.tunnel-name {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
margin: 0;
|
margin: 0 0 8px 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tunnel-badges {
|
.tunnel-badges {
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tunnel-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 80px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 平板端流程图优化 */
|
/* 平板端流程图优化 */
|
||||||
.tunnel-flow {
|
.tunnel-flow {
|
||||||
padding: 18px 15px;
|
padding: 18px 15px;
|
||||||
@@ -976,6 +1165,59 @@ export default {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 对话框按钮移动端优化 */
|
||||||
|
.dialog-footer {
|
||||||
|
padding-top: 15px;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单输入框移动端优化 */
|
||||||
|
.el-input-number {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input-number .el-input__inner {
|
||||||
|
padding-left: 50px;
|
||||||
|
padding-right: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框移动端优化 */
|
||||||
|
.el-dialog {
|
||||||
|
width: 95% !important;
|
||||||
|
margin: 5vh auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input, .el-select {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.tunnel-card {
|
.tunnel-card {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1086,14 +1328,22 @@ export default {
|
|||||||
display: none; /* 垂直布局时隐藏箭头 */
|
display: none; /* 垂直布局时隐藏箭头 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 删除按钮优化 */
|
/* 手机端按钮优化 */
|
||||||
.tunnel-header .el-button {
|
.tunnel-actions {
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
align-self: flex-start;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 70px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn i {
|
||||||
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tunnel-meta {
|
.tunnel-meta {
|
||||||
@@ -1136,6 +1386,17 @@ export default {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕对话框优化 */
|
||||||
|
.dialog-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
width: 100%;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tunnel-card {
|
.tunnel-card {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索用户名或姓名"
|
placeholder="搜索用户名"
|
||||||
prefix-icon="el-icon-search"
|
prefix-icon="el-icon-search"
|
||||||
@keyup.enter.native="handleSearch"
|
@keyup.enter.native="handleSearch"
|
||||||
clearable
|
clearable
|
||||||
@@ -39,9 +39,8 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||||
<el-table-column prop="name" label="姓名" min-width="100"></el-table-column>
|
<el-table-column prop="user" label="用户名" ></el-table-column>
|
||||||
<el-table-column prop="user" label="用户名" min-width="120"></el-table-column>
|
<el-table-column label="流量统计" >
|
||||||
<el-table-column label="流量统计" width="200">
|
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<div class="flow-stats">
|
<div class="flow-stats">
|
||||||
<div class="flow-item">
|
<div class="flow-item">
|
||||||
@@ -246,13 +245,6 @@
|
|||||||
ref="userForm"
|
ref="userForm"
|
||||||
label-width="80px"
|
label-width="80px"
|
||||||
>
|
>
|
||||||
<el-form-item label="姓名" prop="name">
|
|
||||||
<el-input
|
|
||||||
v-model="userForm.name"
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
clearable
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="用户名" prop="user">
|
<el-form-item label="用户名" prop="user">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -744,7 +736,6 @@ export default {
|
|||||||
},
|
},
|
||||||
userForm: {
|
userForm: {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
|
||||||
user: '',
|
user: '',
|
||||||
pwd: '',
|
pwd: '',
|
||||||
status: 1,
|
status: 1,
|
||||||
@@ -754,9 +745,6 @@ export default {
|
|||||||
flowResetTime: 1
|
flowResetTime: 1
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
name: [
|
|
||||||
{ required: true, message: '请输入姓名', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
user: [
|
user: [
|
||||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
{ min: 3, message: '用户名长度至少3位', trigger: 'blur' }
|
{ min: 3, message: '用户名长度至少3位', trigger: 'blur' }
|
||||||
@@ -1326,8 +1314,6 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.user-container {
|
.user-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<!-- 节点卡片展示 -->
|
<!-- 节点卡片展示 -->
|
||||||
<div class="cards-container" v-loading="loading" style="margin-top: 10px;">
|
<div class="cards-container" v-loading="loading" style="margin-top: 20px;">
|
||||||
<div class="cards-grid">
|
<div class="cards-grid">
|
||||||
<div
|
<div
|
||||||
v-for="node in nodeList"
|
v-for="node in nodeList"
|
||||||
@@ -168,6 +168,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 节点与隧道转发IP重要说明 -->
|
||||||
|
<el-alert
|
||||||
|
title="重要提示"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-top: 30px; margin-bottom: 20px;">
|
||||||
|
<template slot="title">
|
||||||
|
<span style="font-weight: 600;">节点与隧道转发IP说明</span>
|
||||||
|
</template>
|
||||||
|
<div style="font-size: 13px; line-height: 1.5; color: #e6a23c;">
|
||||||
|
<p style="margin: 0 0 8px 0;">• 节点IP若与实际入口IP不同,可点击编辑手动更改</p>
|
||||||
|
<p style="margin: 0 0 8px 0;">• 更改节点IP后,<strong>已创建的隧道和转发IP不会自动更改</strong></p>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
:visible.sync="dialogVisible"
|
:visible.sync="dialogVisible"
|
||||||
@@ -883,9 +899,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.node-container {
|
.node-container {
|
||||||
padding: 5px;
|
padding: 20px;
|
||||||
background-color: #f5f7fa;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面头部 */
|
/* 页面头部 */
|
||||||
@@ -1184,25 +1198,25 @@ export default {
|
|||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.node-container {
|
.node-container {
|
||||||
padding: 5px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-grid {
|
.cards-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.charts-container {
|
.charts-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-stats {
|
.traffic-stats {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -1210,58 +1224,58 @@ export default {
|
|||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-card {
|
.node-card {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-header {
|
.node-header {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-name {
|
.node-name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
min-height: 45px;
|
min-height: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-header {
|
.stat-header {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-value {
|
.traffic-value {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.speed-value {
|
.speed-value {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-value {
|
.ip-value {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-actions {
|
.node-actions {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -1280,58 +1294,58 @@ export default {
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.node-container {
|
.node-container {
|
||||||
padding: 3px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-grid {
|
.cards-grid {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-card {
|
.node-card {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.charts-container {
|
.charts-container {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-stats {
|
.traffic-stats {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-actions {
|
.node-actions {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-actions .el-button {
|
.node-actions .el-button {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user