mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
修复bug
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# 转发面板
|
# 哆啦A梦转发面板
|
||||||
|
|
||||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://.com/go-gost/x) 两个开源库,实现了转发面板。
|
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||||
---
|
---
|
||||||
|
|||||||
Binary file not shown.
@@ -1,22 +0,0 @@
|
|||||||
package com.admin.common.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class PageDto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前页码,默认为1
|
|
||||||
*/
|
|
||||||
private Long current = 1L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 每页显示条数,默认为10
|
|
||||||
*/
|
|
||||||
private Long size = 10L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索关键字(可选)
|
|
||||||
*/
|
|
||||||
private String keyword;
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@ public class TunnelDto {
|
|||||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||||
private BigDecimal trafficRatio = new BigDecimal("1.0");
|
private BigDecimal trafficRatio = new BigDecimal("1.0");
|
||||||
|
|
||||||
// 协议类型(隧道转发时使用:tls、tcp、mtls),默认为tls
|
// 协议类型,默认为tls
|
||||||
private String protocol;
|
private String protocol;
|
||||||
|
|
||||||
// TCP监听地址,默认为0.0.0.0
|
// TCP监听地址,默认为0.0.0.0
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ public class TunnelUpdateDto {
|
|||||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||||
private BigDecimal trafficRatio;
|
private BigDecimal trafficRatio;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String protocol;
|
||||||
|
|
||||||
// TCP监听地址
|
// TCP监听地址
|
||||||
|
@NotBlank
|
||||||
private String tcpListenAddr;
|
private String tcpListenAddr;
|
||||||
|
|
||||||
// UDP监听地址
|
// UDP监听地址
|
||||||
|
@NotBlank
|
||||||
private String udpListenAddr;
|
private String udpListenAddr;
|
||||||
}
|
}
|
||||||
@@ -181,6 +181,15 @@ public class GostUtil {
|
|||||||
public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol) {
|
public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol) {
|
||||||
JSONObject dialer = new JSONObject();
|
JSONObject dialer = new JSONObject();
|
||||||
dialer.put("type", protocol);
|
dialer.put("type", protocol);
|
||||||
|
if (Objects.equals(protocol, "quic")){
|
||||||
|
JSONObject metadata = new JSONObject();
|
||||||
|
metadata.put("keepAlive", true);
|
||||||
|
metadata.put("ttl", "10s");
|
||||||
|
dialer.put("metadata", metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JSONObject connector = new JSONObject();
|
JSONObject connector = new JSONObject();
|
||||||
connector.put("type", "relay");
|
connector.put("type", "relay");
|
||||||
@@ -212,6 +221,14 @@ public class GostUtil {
|
|||||||
JSONObject dialer = new JSONObject();
|
JSONObject dialer = new JSONObject();
|
||||||
dialer.put("type", protocol);
|
dialer.put("type", protocol);
|
||||||
|
|
||||||
|
if (Objects.equals(protocol, "quic")){
|
||||||
|
JSONObject metadata = new JSONObject();
|
||||||
|
metadata.put("keepAlive", true);
|
||||||
|
metadata.put("ttl", "10s");
|
||||||
|
dialer.put("metadata", metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
JSONObject connector = new JSONObject();
|
JSONObject connector = new JSONObject();
|
||||||
connector.put("type", "relay");
|
connector.put("type", "relay");
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.admin.common.aop.LogAnnotation;
|
|||||||
import com.admin.common.annotation.RequireRole;
|
import com.admin.common.annotation.RequireRole;
|
||||||
import com.admin.common.dto.ForwardDto;
|
import com.admin.common.dto.ForwardDto;
|
||||||
import com.admin.common.dto.ForwardUpdateDto;
|
import com.admin.common.dto.ForwardUpdateDto;
|
||||||
import com.admin.common.dto.PageDto;
|
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.service.ForwardService;
|
import com.admin.service.ForwardService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.admin.common.annotation.RequireRole;
|
|||||||
import com.admin.common.aop.LogAnnotation;
|
import com.admin.common.aop.LogAnnotation;
|
||||||
import com.admin.common.dto.NodeDto;
|
import com.admin.common.dto.NodeDto;
|
||||||
import com.admin.common.dto.NodeUpdateDto;
|
import com.admin.common.dto.NodeUpdateDto;
|
||||||
import com.admin.common.dto.PageDto;
|
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.admin.controller;
|
|||||||
|
|
||||||
import com.admin.common.aop.LogAnnotation;
|
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.TunnelDto;
|
import com.admin.common.dto.TunnelDto;
|
||||||
import com.admin.common.dto.TunnelUpdateDto;
|
import com.admin.common.dto.TunnelUpdateDto;
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ public class TunnelController extends BaseController {
|
|||||||
@LogAnnotation
|
@LogAnnotation
|
||||||
@RequireRole
|
@RequireRole
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
public R readAll(@RequestBody(required = false) PageDto pageDto) {
|
public R readAll() {
|
||||||
return tunnelService.getAllTunnels();
|
return tunnelService.getAllTunnels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,12 +40,8 @@ public class UserController extends BaseController {
|
|||||||
@LogAnnotation
|
@LogAnnotation
|
||||||
@RequireRole
|
@RequireRole
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
public R readAll(@RequestBody(required = false) PageDto pageDto) {
|
public R readAll() {
|
||||||
// 如果没有传分页参数,使用默认值
|
return userService.getAllUsers();
|
||||||
if (pageDto == null) {
|
|
||||||
pageDto = new PageDto();
|
|
||||||
}
|
|
||||||
return userService.getAllUsers(pageDto);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@LogAnnotation
|
@LogAnnotation
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class Tunnel extends BaseEntity {
|
|||||||
private int flow;
|
private int flow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 协议类型(隧道转发时使用:tls、tcp、mtls)
|
* 协议类型
|
||||||
*/
|
*/
|
||||||
private String protocol;
|
private String protocol;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.admin.service;
|
|||||||
|
|
||||||
import com.admin.common.dto.NodeDto;
|
import com.admin.common.dto.NodeDto;
|
||||||
import com.admin.common.dto.NodeUpdateDto;
|
import com.admin.common.dto.NodeUpdateDto;
|
||||||
import com.admin.common.dto.PageDto;
|
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.entity.Node;
|
import com.admin.entity.Node;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.admin.service;
|
package com.admin.service;
|
||||||
|
|
||||||
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.TunnelUpdateDto;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public interface UserService extends IService<User> {
|
|||||||
|
|
||||||
R createUser(UserDto userDto);
|
R createUser(UserDto userDto);
|
||||||
|
|
||||||
R getAllUsers(PageDto pageDto);
|
R getAllUsers();
|
||||||
|
|
||||||
R updateUser(UserUpdateDto userUpdateDto);
|
R updateUser(UserUpdateDto userUpdateDto);
|
||||||
|
|
||||||
|
|||||||
@@ -147,13 +147,42 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
if (tunnel.getStatus() != TUNNEL_STATUS_ACTIVE) {
|
if (tunnel.getStatus() != TUNNEL_STATUS_ACTIVE) {
|
||||||
return R.err("隧道已禁用,无法更新转发");
|
return R.err("隧道已禁用,无法更新转发");
|
||||||
}
|
}
|
||||||
|
boolean tunnelChanged = isTunnelChanged(existForward, forwardUpdateDto);
|
||||||
// 4. 检查权限和限制
|
// 4. 检查权限和限制
|
||||||
UserPermissionResult permissionResult = null;
|
UserPermissionResult permissionResult = null;
|
||||||
if (isTunnelChanged(existForward, forwardUpdateDto)) {
|
if (tunnelChanged) {
|
||||||
permissionResult = checkUserPermissions(currentUser, tunnel, forwardUpdateDto.getId());
|
// 管理员操作用户转发时,需要检查原用户是否有新隧道权限
|
||||||
if (permissionResult.isHasError()) {
|
if (currentUser.getRoleId() == ADMIN_ROLE_ID) {
|
||||||
return R.err(permissionResult.getErrorMessage());
|
// 获取原转发用户的信息
|
||||||
|
User originalUser = userService.getById(existForward.getUserId());
|
||||||
|
if (originalUser == null) {
|
||||||
|
return R.err("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查原用户是否有新隧道权限
|
||||||
|
UserTunnel userTunnel = getUserTunnel(existForward.getUserId(), tunnel.getId().intValue());
|
||||||
|
if (userTunnel == null) {
|
||||||
|
return R.err("用户没有该隧道权限");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查隧道权限到期时间
|
||||||
|
if (userTunnel.getExpTime() != null && userTunnel.getExpTime() <= System.currentTimeMillis()) {
|
||||||
|
return R.err("用户的该隧道权限已到期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查原用户的流量和转发数量限制
|
||||||
|
R quotaCheckResult = checkForwardQuota(existForward.getUserId(), tunnel.getId().intValue(), userTunnel, originalUser, forwardUpdateDto.getId());
|
||||||
|
if (quotaCheckResult.getCode() != 0) {
|
||||||
|
return R.err("用户" + quotaCheckResult.getMsg());
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionResult = UserPermissionResult.success(userTunnel.getSpeedId(), userTunnel);
|
||||||
|
} else {
|
||||||
|
// 普通用户检查自己的权限
|
||||||
|
permissionResult = checkUserPermissions(currentUser, tunnel, forwardUpdateDto.getId());
|
||||||
|
if (permissionResult.isHasError()) {
|
||||||
|
return R.err(permissionResult.getErrorMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,16 +210,12 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
|
|
||||||
// 8. 调用Gost服务更新转发
|
// 8. 调用Gost服务更新转发
|
||||||
R gostResult;
|
R gostResult;
|
||||||
if (isTunnelChanged(existForward, forwardUpdateDto)) {
|
if (tunnelChanged) {
|
||||||
// 隧道变化时:先删除原配置,再创建新配置
|
// 隧道变化时:先删除原配置,再创建新配置
|
||||||
gostResult = updateGostServicesWithTunnelChange(existForward, updatedForward, tunnel,
|
gostResult = updateGostServicesWithTunnelChange(existForward, updatedForward, tunnel, permissionResult != null ? permissionResult.getLimiter() : null, nodeInfo, userTunnel);
|
||||||
permissionResult != null ? permissionResult.getLimiter() : null,
|
|
||||||
nodeInfo, userTunnel);
|
|
||||||
} else {
|
} else {
|
||||||
// 隧道未变化时:直接更新配置
|
// 隧道未变化时:直接更新配置
|
||||||
gostResult = updateGostServices(updatedForward, tunnel,
|
gostResult = updateGostServices(updatedForward, tunnel, permissionResult != null ? permissionResult.getLimiter() : null, nodeInfo, userTunnel);
|
||||||
permissionResult != null ? permissionResult.getLimiter() : null,
|
|
||||||
nodeInfo, userTunnel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gostResult.getCode() != 0) {
|
if (gostResult.getCode() != 0) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import cn.hutool.core.util.IdUtil;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.admin.common.dto.NodeDto;
|
import com.admin.common.dto.NodeDto;
|
||||||
import com.admin.common.dto.NodeUpdateDto;
|
import com.admin.common.dto.NodeUpdateDto;
|
||||||
import com.admin.common.dto.PageDto;
|
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
import com.admin.entity.Node;
|
import com.admin.entity.Node;
|
||||||
import com.admin.entity.Tunnel;
|
import com.admin.entity.Tunnel;
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package com.admin.service.impl;
|
package com.admin.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.admin.common.dto.GostDto;
|
import com.admin.common.dto.*;
|
||||||
import com.admin.common.dto.TunnelDto;
|
|
||||||
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.GostUtil;
|
import com.admin.common.utils.GostUtil;
|
||||||
@@ -30,10 +27,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,35 +169,50 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 验证隧道名称唯一性(排除自身)
|
// 2. 验证隧道名称唯一性(排除自身)
|
||||||
R nameValidationResult = validateTunnelNameUniquenessForUpdate(
|
R nameValidationResult = validateTunnelNameUniquenessForUpdate(tunnelUpdateDto.getName(), tunnelUpdateDto.getId());
|
||||||
tunnelUpdateDto.getName(), tunnelUpdateDto.getId());
|
|
||||||
if (nameValidationResult.getCode() != 0) {
|
if (nameValidationResult.getCode() != 0) {
|
||||||
return nameValidationResult;
|
return nameValidationResult;
|
||||||
}
|
}
|
||||||
|
int up = 0;
|
||||||
|
if (!Objects.equals(existingTunnel.getTcpListenAddr(), tunnelUpdateDto.getTcpListenAddr()) ||
|
||||||
|
!Objects.equals(existingTunnel.getUdpListenAddr(), tunnelUpdateDto.getUdpListenAddr()) ||
|
||||||
|
!Objects.equals(existingTunnel.getProtocol(), tunnelUpdateDto.getProtocol())) {
|
||||||
|
up++;
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 更新允许修改的字段
|
// 5. 更新允许修改的字段
|
||||||
existingTunnel.setName(tunnelUpdateDto.getName());
|
existingTunnel.setName(tunnelUpdateDto.getName());
|
||||||
existingTunnel.setFlow(tunnelUpdateDto.getFlow());
|
existingTunnel.setFlow(tunnelUpdateDto.getFlow());
|
||||||
|
existingTunnel.setTcpListenAddr(tunnelUpdateDto.getTcpListenAddr());
|
||||||
// 更新流量倍率
|
existingTunnel.setUdpListenAddr(tunnelUpdateDto.getUdpListenAddr());
|
||||||
if (tunnelUpdateDto.getTrafficRatio() != null) {
|
existingTunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio());
|
||||||
existingTunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio());
|
existingTunnel.setProtocol(tunnelUpdateDto.getProtocol());
|
||||||
}
|
this.updateById(existingTunnel);
|
||||||
|
int err = 0;
|
||||||
// 更新TCP和UDP监听地址
|
if (up == 1){
|
||||||
if (StrUtil.isNotBlank(tunnelUpdateDto.getTcpListenAddr())) {
|
List<Forward> tunnel = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", tunnelUpdateDto.getId()));
|
||||||
existingTunnel.setTcpListenAddr(tunnelUpdateDto.getTcpListenAddr());
|
if (!tunnel.isEmpty()) {
|
||||||
}
|
for (Forward forward : tunnel) {
|
||||||
if (StrUtil.isNotBlank(tunnelUpdateDto.getUdpListenAddr())) {
|
ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto();
|
||||||
existingTunnel.setUdpListenAddr(tunnelUpdateDto.getUdpListenAddr());
|
forwardUpdateDto.setId(forward.getId());
|
||||||
|
forwardUpdateDto.setUserId(forward.getUserId());
|
||||||
|
forwardUpdateDto.setName(forward.getName());
|
||||||
|
forwardUpdateDto.setTunnelId(forward.getTunnelId());
|
||||||
|
forwardUpdateDto.setRemoteAddr(forward.getRemoteAddr());
|
||||||
|
forwardUpdateDto.setStrategy(forward.getStrategy());
|
||||||
|
forwardUpdateDto.setInPort(forward.getInPort());
|
||||||
|
R r = forwardService.updateForward(forwardUpdateDto);
|
||||||
|
if (r.getCode() != 0){
|
||||||
|
err++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err != 0) {
|
||||||
// 6. 保存更新
|
return R.err("隧道信息更新成功,但部分转发同步更新失败");
|
||||||
boolean result = this.updateById(existingTunnel);
|
}
|
||||||
return result ? R.ok("隧道更新成功") : R.err("隧道更新失败");
|
return R.ok("隧道更新成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -425,9 +434,8 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
|
|
||||||
// 验证协议类型
|
// 验证协议类型
|
||||||
String protocol = tunnelDto.getProtocol();
|
String protocol = tunnelDto.getProtocol();
|
||||||
if (StrUtil.isNotBlank(protocol) &&
|
if (StrUtil.isBlank(protocol)) {
|
||||||
!protocol.equals("tls") && !protocol.equals("tcp") && !protocol.equals("mtls") && !protocol.equals("wss")) {
|
return R.err("协议类型必选");
|
||||||
return R.err("协议类型只能为tls、tcp、wss或mtls");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证出口节点是否存在
|
// 验证出口节点是否存在
|
||||||
|
|||||||
@@ -183,21 +183,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
/**
|
/**
|
||||||
* 获取所有用户(分页)
|
* 获取所有用户(分页)
|
||||||
* 支持关键字搜索,排除管理员用户,清除密码信息
|
* 支持关键字搜索,排除管理员用户,清除密码信息
|
||||||
*
|
*
|
||||||
* @param pageDto 分页查询数据传输对象
|
|
||||||
* @return 分页用户列表响应
|
* @return 分页用户列表响应
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public R getAllUsers(PageDto pageDto) {
|
public R getAllUsers() {
|
||||||
// 1. 构建分页查询
|
return R.ok(this.list(new QueryWrapper<User>().ne("role_id", ADMIN_ROLE_ID)));
|
||||||
Page<User> page = new Page<>(pageDto.getCurrent(), pageDto.getSize());
|
|
||||||
QueryWrapper<User> queryWrapper = buildUserQueryWrapper(pageDto);
|
|
||||||
|
|
||||||
// 2. 执行查询并处理结果
|
|
||||||
Page<User> userPage = this.page(page, queryWrapper);
|
|
||||||
clearUserPasswords(userPage.getRecords());
|
|
||||||
|
|
||||||
return R.ok(userPage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -446,39 +437,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建用户查询条件
|
|
||||||
*
|
|
||||||
* @param pageDto 分页查询DTO
|
|
||||||
* @return 查询条件包装器
|
|
||||||
*/
|
|
||||||
private QueryWrapper<User> buildUserQueryWrapper(PageDto pageDto) {
|
|
||||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
|
||||||
|
|
||||||
// 关键字搜索
|
|
||||||
if (StrUtil.isNotBlank(pageDto.getKeyword())) {
|
|
||||||
queryWrapper.and(wrapper -> wrapper
|
|
||||||
.like("user", pageDto.getKeyword())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排除管理员用户
|
|
||||||
queryWrapper.ne("id", ADMIN_USER_ID);
|
|
||||||
|
|
||||||
// 按更新时间降序排列
|
|
||||||
queryWrapper.orderByDesc("updated_time");
|
|
||||||
|
|
||||||
return queryWrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除用户列表中的密码信息
|
|
||||||
*
|
|
||||||
* @param users 用户列表
|
|
||||||
*/
|
|
||||||
private void clearUserPasswords(List<User> users) {
|
|
||||||
users.forEach(user -> user.setPwd(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查用户是否存在
|
* 检查用户是否存在
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_API_BASE=http://127.0.0.1:6365
|
VITE_API_BASE=http://192.168.100.8:6365
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
"@heroui/switch": "^2.2.21",
|
"@heroui/switch": "^2.2.21",
|
||||||
"@heroui/system": "2.4.19",
|
"@heroui/system": "2.4.19",
|
||||||
"@heroui/table": "^2.2.24",
|
"@heroui/table": "^2.2.24",
|
||||||
"@heroui/tabs": "^2.2.21",
|
|
||||||
"@heroui/theme": "2.4.19",
|
"@heroui/theme": "2.4.19",
|
||||||
"@heroui/use-theme": "2.1.10",
|
"@heroui/use-theme": "2.1.10",
|
||||||
"@nextui-org/system": "^2.4.6",
|
"@nextui-org/system": "^2.4.6",
|
||||||
|
|||||||
@@ -243,11 +243,11 @@ export default function AdminLayout({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-50 dark:bg-black">
|
<div className={`flex ${isMobile ? 'min-h-screen' : 'h-screen'} bg-gray-50 dark:bg-black`}>
|
||||||
{/* 移动端遮罩层 */}
|
{/* 移动端遮罩层 */}
|
||||||
{isMobile && mobileMenuVisible && (
|
{isMobile && mobileMenuVisible && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
className="fixed inset-0 backdrop-blur-sm bg-white/50 dark:bg-black/30 z-40"
|
||||||
onClick={hideMobileMenu}
|
onClick={hideMobileMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -263,7 +263,8 @@ export default function AdminLayout({
|
|||||||
z-50
|
z-50
|
||||||
transition-transform duration-300 ease-in-out
|
transition-transform duration-300 ease-in-out
|
||||||
flex flex-col
|
flex flex-col
|
||||||
h-full
|
${isMobile ? 'h-screen' : 'h-full'}
|
||||||
|
${isMobile ? 'top-0 left-0' : ''}
|
||||||
`}>
|
`}>
|
||||||
{/* Logo 区域 */}
|
{/* Logo 区域 */}
|
||||||
<div className="px-3 py-3 h-14 flex items-center">
|
<div className="px-3 py-3 h-14 flex items-center">
|
||||||
@@ -277,7 +278,7 @@ export default function AdminLayout({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 菜单导航 */}
|
{/* 菜单导航 */}
|
||||||
<nav className="flex-1 px-4 py-6">
|
<nav className="flex-1 px-4 py-6 overflow-y-auto">
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{filteredMenuItems.map((item) => {
|
{filteredMenuItems.map((item) => {
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
@@ -306,7 +307,7 @@ export default function AdminLayout({
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* 底部版权信息 */}
|
{/* 底部版权信息 */}
|
||||||
<div className="px-4 py-2 pb-4 mt-auto">
|
<div className="px-4 py-2 pb-4 mt-auto flex-shrink-0">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
@@ -324,7 +325,7 @@ export default function AdminLayout({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className={`flex flex-col flex-1 ${isMobile ? 'min-h-0' : 'h-full overflow-hidden'}`}>
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
|
<header className="bg-white dark:bg-black shadow-md border-b border-gray-200 dark:border-gray-600 h-14 flex items-center justify-between px-4 lg:px-6 relative z-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -387,7 +388,7 @@ export default function AdminLayout({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容 */}
|
{/* 主内容 */}
|
||||||
<main className="flex-1 overflow-y-auto bg-gray-100 dark:bg-black">
|
<main className={`flex-1 bg-gray-100 dark:bg-black ${isMobile ? '' : 'overflow-y-auto'}`}>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -122,6 +122,20 @@ export default function ForwardPage() {
|
|||||||
const [forwards, setForwards] = useState<Forward[]>([]);
|
const [forwards, setForwards] = useState<Forward[]>([]);
|
||||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||||
|
|
||||||
|
// 检测是否为移动端
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
setIsMobile(window.innerWidth < 768);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 显示模式状态 - 从localStorage读取,默认为分类显示
|
// 显示模式状态 - 从localStorage读取,默认为分类显示
|
||||||
const [viewMode, setViewMode] = useState<'grouped' | 'direct'>(() => {
|
const [viewMode, setViewMode] = useState<'grouped' | 'direct'>(() => {
|
||||||
try {
|
try {
|
||||||
@@ -1009,12 +1023,21 @@ export default function ForwardPage() {
|
|||||||
const handleDragEnd = async (event: DragEndEvent) => {
|
const handleDragEnd = async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
|
||||||
if (!over || active.id === over.id) return;
|
if (!active || !over || active.id === over.id) return;
|
||||||
|
|
||||||
const oldIndex = forwardOrder.indexOf(Number(active.id));
|
// 确保 forwardOrder 存在且有效
|
||||||
const newIndex = forwardOrder.indexOf(Number(over.id));
|
if (!forwardOrder || forwardOrder.length === 0) return;
|
||||||
|
|
||||||
if (oldIndex !== -1 && newIndex !== -1) {
|
const activeId = Number(active.id);
|
||||||
|
const overId = Number(over.id);
|
||||||
|
|
||||||
|
// 检查 ID 是否有效
|
||||||
|
if (isNaN(activeId) || isNaN(overId)) return;
|
||||||
|
|
||||||
|
const oldIndex = forwardOrder.indexOf(activeId);
|
||||||
|
const newIndex = forwardOrder.indexOf(overId);
|
||||||
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
||||||
const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);
|
const newOrder = arrayMove(forwardOrder, oldIndex, newIndex);
|
||||||
setForwardOrder(newOrder);
|
setForwardOrder(newOrder);
|
||||||
|
|
||||||
@@ -1052,7 +1075,7 @@ export default function ForwardPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 传感器配置
|
// 传感器配置 - 使用默认配置避免错误
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
@@ -1062,6 +1085,11 @@ export default function ForwardPage() {
|
|||||||
|
|
||||||
// 根据排序顺序获取转发列表
|
// 根据排序顺序获取转发列表
|
||||||
const getSortedForwards = (): Forward[] => {
|
const getSortedForwards = (): Forward[] => {
|
||||||
|
// 确保 forwards 数组存在且有效
|
||||||
|
if (!forwards || forwards.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// 在平铺模式下,只显示当前用户的转发
|
// 在平铺模式下,只显示当前用户的转发
|
||||||
let filteredForwards = forwards;
|
let filteredForwards = forwards;
|
||||||
if (viewMode === 'direct') {
|
if (viewMode === 'direct') {
|
||||||
@@ -1071,6 +1099,11 @@ export default function ForwardPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保过滤后的转发列表有效
|
||||||
|
if (!filteredForwards || filteredForwards.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// 优先使用数据库中的 inx 字段进行排序
|
// 优先使用数据库中的 inx 字段进行排序
|
||||||
const sortedForwards = [...filteredForwards].sort((a, b) => {
|
const sortedForwards = [...filteredForwards].sort((a, b) => {
|
||||||
const aInx = a.inx ?? 0;
|
const aInx = a.inx ?? 0;
|
||||||
@@ -1079,7 +1112,7 @@ export default function ForwardPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
// 如果数据库中没有排序信息,则使用本地存储的顺序
|
||||||
if (forwardOrder.length > 0 && sortedForwards.every(f => f.inx === undefined || f.inx === 0)) {
|
if (forwardOrder && forwardOrder.length > 0 && sortedForwards.every(f => f.inx === undefined || f.inx === 0)) {
|
||||||
const forwardMap = new Map(filteredForwards.map(f => [f.id, f]));
|
const forwardMap = new Map(filteredForwards.map(f => [f.id, f]));
|
||||||
const localSortedForwards: Forward[] = [];
|
const localSortedForwards: Forward[] = [];
|
||||||
|
|
||||||
@@ -1105,6 +1138,11 @@ export default function ForwardPage() {
|
|||||||
|
|
||||||
// 可拖拽的转发卡片组件
|
// 可拖拽的转发卡片组件
|
||||||
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
||||||
|
// 确保 forward 对象有效
|
||||||
|
if (!forward || !forward.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -1115,20 +1153,20 @@ export default function ForwardPage() {
|
|||||||
} = useSortable({ id: forward.id });
|
} = useSortable({ id: forward.id });
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
||||||
transition,
|
transition: transition || undefined,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
<div ref={setNodeRef} style={style} {...attributes}>
|
||||||
{renderForwardCard(forward)}
|
{renderForwardCard(forward, listeners)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染转发卡片
|
// 渲染转发卡片
|
||||||
const renderForwardCard = (forward: Forward) => {
|
const renderForwardCard = (forward: Forward, listeners?: any) => {
|
||||||
const statusDisplay = getStatusDisplay(forward.status);
|
const statusDisplay = getStatusDisplay(forward.status);
|
||||||
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
||||||
|
|
||||||
@@ -1142,7 +1180,16 @@ export default function ForwardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 ml-2">
|
<div className="flex items-center gap-1.5 ml-2">
|
||||||
{viewMode === 'direct' && (
|
{viewMode === 'direct' && (
|
||||||
<div className="cursor-grab active:cursor-grabbing p-1 text-default-400 hover:text-default-600 transition-colors opacity-0 group-hover:opacity-100">
|
<div
|
||||||
|
className={`cursor-grab active:cursor-grabbing p-2 text-default-400 hover:text-default-600 transition-colors touch-manipulation ${
|
||||||
|
isMobile
|
||||||
|
? 'opacity-100' // 移动端始终显示
|
||||||
|
: 'opacity-0 group-hover:opacity-100 sm:opacity-0 sm:group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
{...listeners}
|
||||||
|
title={isMobile ? "长按拖拽排序" : "拖拽排序"}
|
||||||
|
style={{ touchAction: 'none' }}
|
||||||
|
>
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
|
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -1363,6 +1410,7 @@ export default function ForwardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* 根据显示模式渲染不同内容 */}
|
{/* 根据显示模式渲染不同内容 */}
|
||||||
{viewMode === 'grouped' ? (
|
{viewMode === 'grouped' ? (
|
||||||
/* 按用户和隧道分组的转发列表 */
|
/* 按用户和隧道分组的转发列表 */
|
||||||
@@ -1420,7 +1468,7 @@ export default function ForwardPage() {
|
|||||||
className="shadow-none border border-divider"
|
className="shadow-none border border-divider"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 p-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 p-4">
|
||||||
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward))}
|
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward, undefined))}
|
||||||
</div>
|
</div>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
))}
|
))}
|
||||||
@@ -1454,14 +1502,17 @@ export default function ForwardPage() {
|
|||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragStart={() => {}} // 添加空的 onDragStart 处理器
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={getSortedForwards().map(f => f.id)}
|
items={getSortedForwards().map(f => f.id || 0).filter(id => id > 0)}
|
||||||
strategy={rectSortingStrategy}
|
strategy={rectSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{getSortedForwards().map((forward) => (
|
{getSortedForwards().map((forward) => (
|
||||||
<SortableForwardCard key={forward.id} forward={forward} />
|
forward && forward.id ? (
|
||||||
|
<SortableForwardCard key={forward.id} forward={forward} />
|
||||||
|
) : null
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Textarea } from "@heroui/input";
|
|||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
|
||||||
import { Chip } from "@heroui/chip";
|
import { Chip } from "@heroui/chip";
|
||||||
import { Spinner } from "@heroui/spinner";
|
import { Spinner } from "@heroui/spinner";
|
||||||
|
import { Alert } from "@heroui/alert";
|
||||||
import { Progress } from "@heroui/progress";
|
import { Progress } from "@heroui/progress";
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -871,6 +872,28 @@ export default function NodePage() {
|
|||||||
max={65535}
|
max={65535}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址"
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
|
<Alert
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
description="入口ip是用于展示在转发页面,面向用户的访问地址"
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
|
<Alert
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
description="实在理解不到说明你没这个需求,都填节点的服务器ip就行"
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -770,12 +770,14 @@ export default function TunnelPage() {
|
|||||||
isInvalid={!!errors.protocol}
|
isInvalid={!!errors.protocol}
|
||||||
errorMessage={errors.protocol}
|
errorMessage={errors.protocol}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
|
||||||
>
|
>
|
||||||
<SelectItem key="tls">TLS</SelectItem>
|
<SelectItem key="tls">TLS</SelectItem>
|
||||||
<SelectItem key="wss">WSS</SelectItem>
|
<SelectItem key="wss">WSS</SelectItem>
|
||||||
<SelectItem key="tcp">TCP</SelectItem>
|
<SelectItem key="tcp">TCP</SelectItem>
|
||||||
<SelectItem key="mtls">mTLS</SelectItem>
|
<SelectItem key="mtls">mTLS</SelectItem>
|
||||||
|
<SelectItem key="mwss">mWSS</SelectItem>
|
||||||
|
<SelectItem key="mtcp">mTCP</SelectItem>
|
||||||
|
<SelectItem key="quic">QUIC</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ import {
|
|||||||
useDisclosure
|
useDisclosure
|
||||||
} from "@heroui/modal";
|
} from "@heroui/modal";
|
||||||
import { Chip } from "@heroui/chip";
|
import { Chip } from "@heroui/chip";
|
||||||
import { Pagination } from "@heroui/pagination";
|
|
||||||
import { Select, SelectItem } from "@heroui/select";
|
import { Select, SelectItem } from "@heroui/select";
|
||||||
import { RadioGroup, Radio } from "@heroui/radio";
|
import { RadioGroup, Radio } from "@heroui/radio";
|
||||||
import { DatePicker } from "@heroui/date-picker";
|
import { DatePicker } from "@heroui/date-picker";
|
||||||
import { Spinner } from "@heroui/spinner";
|
import { Spinner } from "@heroui/spinner";
|
||||||
import { Progress } from "@heroui/progress";
|
import { Progress } from "@heroui/progress";
|
||||||
import { Tabs, Tab } from "@heroui/tabs";
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
@@ -191,13 +190,7 @@ export default function UserPage() {
|
|||||||
|
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
const data = response.data || {};
|
const data = response.data || {};
|
||||||
setUsers(data.records || []);
|
setUsers(data || []);
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
total: data.total || 0,
|
|
||||||
current: data.current || 1,
|
|
||||||
size: data.size || 10
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.msg || '获取用户列表失败');
|
toast.error(response.msg || '获取用户列表失败');
|
||||||
}
|
}
|
||||||
@@ -748,18 +741,6 @@ export default function UserPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 分页 */}
|
|
||||||
{pagination.total > 0 && (
|
|
||||||
<div className="flex justify-center mt-6">
|
|
||||||
<Pagination
|
|
||||||
total={Math.ceil(pagination.total / pagination.size)}
|
|
||||||
page={pagination.current}
|
|
||||||
onChange={(page) => setPagination(prev => ({ ...prev, current: page }))}
|
|
||||||
showControls
|
|
||||||
showShadow
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 用户表单模态框 */}
|
{/* 用户表单模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -877,16 +858,22 @@ export default function UserPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isTunnelModalOpen}
|
isOpen={isTunnelModalOpen}
|
||||||
onClose={onTunnelModalClose}
|
onClose={onTunnelModalClose}
|
||||||
size="5xl"
|
size="4xl"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
|
isDismissable={false}
|
||||||
|
classNames={{
|
||||||
|
base: "max-w-[95vw] sm:max-w-4xl"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
用户 {currentUser?.user} 的隧道权限管理
|
用户 {currentUser?.user} 的隧道权限管理
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Tabs defaultSelectedKey="assign">
|
<div className="space-y-6">
|
||||||
<Tab key="assign" title="分配新权限">
|
{/* 分配新权限部分 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">分配新权限</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Select
|
<Select
|
||||||
@@ -991,9 +978,11 @@ export default function UserPage() {
|
|||||||
分配权限
|
分配权限
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</div>
|
||||||
|
|
||||||
<Tab key="list" title="已有权限">
|
{/* 已有权限部分 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">已有权限</h3>
|
||||||
<Table
|
<Table
|
||||||
aria-label="用户隧道权限列表"
|
aria-label="用户隧道权限列表"
|
||||||
classNames={{
|
classNames={{
|
||||||
@@ -1083,8 +1072,8 @@ export default function UserPage() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Tab>
|
</div>
|
||||||
</Tabs>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onTunnelModalClose}>
|
<Button onPress={onTunnelModalClose}>
|
||||||
@@ -1100,6 +1089,7 @@ export default function UserPage() {
|
|||||||
onClose={onEditTunnelModalClose}
|
onClose={onEditTunnelModalClose}
|
||||||
size="2xl"
|
size="2xl"
|
||||||
scrollBehavior="outside"
|
scrollBehavior="outside"
|
||||||
|
isDismissable={false}
|
||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
|
|||||||
@@ -1,3 +1,33 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
|
||||||
|
/* 移动端视口高度优化 */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解决移动端100vh问题 */
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
.h-screen {
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端滚动优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
* {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@config "../../tailwind.config.js"
|
@config "../../tailwind.config.js"
|
||||||
Reference in New Issue
Block a user