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) 两个开源库,实现了转发面板。
|
||||
---
|
||||
|
||||
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")
|
||||
private BigDecimal trafficRatio = new BigDecimal("1.0");
|
||||
|
||||
// 协议类型(隧道转发时使用:tls、tcp、mtls),默认为tls
|
||||
// 协议类型,默认为tls
|
||||
private String protocol;
|
||||
|
||||
// TCP监听地址,默认为0.0.0.0
|
||||
|
||||
@@ -26,9 +26,14 @@ public class TunnelUpdateDto {
|
||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||
private BigDecimal trafficRatio;
|
||||
|
||||
@NotBlank
|
||||
private String protocol;
|
||||
|
||||
// TCP监听地址
|
||||
@NotBlank
|
||||
private String tcpListenAddr;
|
||||
|
||||
// UDP监听地址
|
||||
@NotBlank
|
||||
private String udpListenAddr;
|
||||
}
|
||||
@@ -181,6 +181,15 @@ public class GostUtil {
|
||||
public static GostDto AddChains(Long node_id, String name, String remoteAddr, String protocol) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
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();
|
||||
connector.put("type", "relay");
|
||||
@@ -212,6 +221,14 @@ public class GostUtil {
|
||||
JSONObject dialer = new JSONObject();
|
||||
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();
|
||||
connector.put("type", "relay");
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.dto.ForwardDto;
|
||||
import com.admin.common.dto.ForwardUpdateDto;
|
||||
import com.admin.common.dto.PageDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.service.ForwardService;
|
||||
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.dto.NodeDto;
|
||||
import com.admin.common.dto.NodeUpdateDto;
|
||||
import com.admin.common.dto.PageDto;
|
||||
import com.admin.common.lang.R;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.admin.controller;
|
||||
|
||||
import com.admin.common.aop.LogAnnotation;
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.dto.PageDto;
|
||||
import com.admin.common.dto.TunnelDto;
|
||||
import com.admin.common.dto.TunnelUpdateDto;
|
||||
|
||||
@@ -47,7 +46,7 @@ public class TunnelController extends BaseController {
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/list")
|
||||
public R readAll(@RequestBody(required = false) PageDto pageDto) {
|
||||
public R readAll() {
|
||||
return tunnelService.getAllTunnels();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,8 @@ public class UserController extends BaseController {
|
||||
@LogAnnotation
|
||||
@RequireRole
|
||||
@PostMapping("/list")
|
||||
public R readAll(@RequestBody(required = false) PageDto pageDto) {
|
||||
// 如果没有传分页参数,使用默认值
|
||||
if (pageDto == null) {
|
||||
pageDto = new PageDto();
|
||||
}
|
||||
return userService.getAllUsers(pageDto);
|
||||
public R readAll() {
|
||||
return userService.getAllUsers();
|
||||
}
|
||||
|
||||
@LogAnnotation
|
||||
|
||||
@@ -55,7 +55,7 @@ public class Tunnel extends BaseEntity {
|
||||
private int flow;
|
||||
|
||||
/**
|
||||
* 协议类型(隧道转发时使用:tls、tcp、mtls)
|
||||
* 协议类型
|
||||
*/
|
||||
private String protocol;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.admin.service;
|
||||
|
||||
import com.admin.common.dto.NodeDto;
|
||||
import com.admin.common.dto.NodeUpdateDto;
|
||||
import com.admin.common.dto.PageDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.Node;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.admin.service;
|
||||
|
||||
import com.admin.common.dto.PageDto;
|
||||
import com.admin.common.dto.TunnelDto;
|
||||
import com.admin.common.dto.TunnelUpdateDto;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface UserService extends IService<User> {
|
||||
|
||||
R createUser(UserDto userDto);
|
||||
|
||||
R getAllUsers(PageDto pageDto);
|
||||
R getAllUsers();
|
||||
|
||||
R updateUser(UserUpdateDto userUpdateDto);
|
||||
|
||||
|
||||
@@ -147,13 +147,42 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
if (tunnel.getStatus() != TUNNEL_STATUS_ACTIVE) {
|
||||
return R.err("隧道已禁用,无法更新转发");
|
||||
}
|
||||
|
||||
boolean tunnelChanged = isTunnelChanged(existForward, forwardUpdateDto);
|
||||
// 4. 检查权限和限制
|
||||
UserPermissionResult permissionResult = null;
|
||||
if (isTunnelChanged(existForward, forwardUpdateDto)) {
|
||||
permissionResult = checkUserPermissions(currentUser, tunnel, forwardUpdateDto.getId());
|
||||
if (permissionResult.isHasError()) {
|
||||
return R.err(permissionResult.getErrorMessage());
|
||||
if (tunnelChanged) {
|
||||
// 管理员操作用户转发时,需要检查原用户是否有新隧道权限
|
||||
if (currentUser.getRoleId() == ADMIN_ROLE_ID) {
|
||||
// 获取原转发用户的信息
|
||||
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服务更新转发
|
||||
R gostResult;
|
||||
if (isTunnelChanged(existForward, forwardUpdateDto)) {
|
||||
if (tunnelChanged) {
|
||||
// 隧道变化时:先删除原配置,再创建新配置
|
||||
gostResult = updateGostServicesWithTunnelChange(existForward, updatedForward, tunnel,
|
||||
permissionResult != null ? permissionResult.getLimiter() : null,
|
||||
nodeInfo, userTunnel);
|
||||
gostResult = updateGostServicesWithTunnelChange(existForward, updatedForward, tunnel, permissionResult != null ? permissionResult.getLimiter() : null, nodeInfo, userTunnel);
|
||||
} else {
|
||||
// 隧道未变化时:直接更新配置
|
||||
gostResult = updateGostServices(updatedForward, tunnel,
|
||||
permissionResult != null ? permissionResult.getLimiter() : null,
|
||||
nodeInfo, userTunnel);
|
||||
gostResult = updateGostServices(updatedForward, tunnel, permissionResult != null ? permissionResult.getLimiter() : null, nodeInfo, userTunnel);
|
||||
}
|
||||
|
||||
if (gostResult.getCode() != 0) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.NodeDto;
|
||||
import com.admin.common.dto.NodeUpdateDto;
|
||||
import com.admin.common.dto.PageDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.entity.Tunnel;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.admin.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.dto.TunnelDto;
|
||||
import com.admin.common.dto.TunnelListDto;
|
||||
import com.admin.common.dto.TunnelUpdateDto;
|
||||
import com.admin.common.dto.*;
|
||||
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
@@ -30,10 +27,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -175,35 +169,50 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
}
|
||||
|
||||
// 2. 验证隧道名称唯一性(排除自身)
|
||||
R nameValidationResult = validateTunnelNameUniquenessForUpdate(
|
||||
tunnelUpdateDto.getName(), tunnelUpdateDto.getId());
|
||||
R nameValidationResult = validateTunnelNameUniquenessForUpdate(tunnelUpdateDto.getName(), tunnelUpdateDto.getId());
|
||||
if (nameValidationResult.getCode() != 0) {
|
||||
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. 更新允许修改的字段
|
||||
existingTunnel.setName(tunnelUpdateDto.getName());
|
||||
existingTunnel.setFlow(tunnelUpdateDto.getFlow());
|
||||
|
||||
// 更新流量倍率
|
||||
if (tunnelUpdateDto.getTrafficRatio() != null) {
|
||||
existingTunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio());
|
||||
}
|
||||
|
||||
// 更新TCP和UDP监听地址
|
||||
if (StrUtil.isNotBlank(tunnelUpdateDto.getTcpListenAddr())) {
|
||||
existingTunnel.setTcpListenAddr(tunnelUpdateDto.getTcpListenAddr());
|
||||
}
|
||||
if (StrUtil.isNotBlank(tunnelUpdateDto.getUdpListenAddr())) {
|
||||
existingTunnel.setUdpListenAddr(tunnelUpdateDto.getUdpListenAddr());
|
||||
existingTunnel.setTcpListenAddr(tunnelUpdateDto.getTcpListenAddr());
|
||||
existingTunnel.setUdpListenAddr(tunnelUpdateDto.getUdpListenAddr());
|
||||
existingTunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio());
|
||||
existingTunnel.setProtocol(tunnelUpdateDto.getProtocol());
|
||||
this.updateById(existingTunnel);
|
||||
int err = 0;
|
||||
if (up == 1){
|
||||
List<Forward> tunnel = forwardService.list(new QueryWrapper<Forward>().eq("tunnel_id", tunnelUpdateDto.getId()));
|
||||
if (!tunnel.isEmpty()) {
|
||||
for (Forward forward : tunnel) {
|
||||
ForwardUpdateDto forwardUpdateDto = new ForwardUpdateDto();
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 6. 保存更新
|
||||
boolean result = this.updateById(existingTunnel);
|
||||
return result ? R.ok("隧道更新成功") : R.err("隧道更新失败");
|
||||
if (err != 0) {
|
||||
return R.err("隧道信息更新成功,但部分转发同步更新失败");
|
||||
}
|
||||
return R.ok("隧道更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,9 +434,8 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
|
||||
// 验证协议类型
|
||||
String protocol = tunnelDto.getProtocol();
|
||||
if (StrUtil.isNotBlank(protocol) &&
|
||||
!protocol.equals("tls") && !protocol.equals("tcp") && !protocol.equals("mtls") && !protocol.equals("wss")) {
|
||||
return R.err("协议类型只能为tls、tcp、wss或mtls");
|
||||
if (StrUtil.isBlank(protocol)) {
|
||||
return R.err("协议类型必选");
|
||||
}
|
||||
|
||||
// 验证出口节点是否存在
|
||||
|
||||
@@ -183,21 +183,12 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
/**
|
||||
* 获取所有用户(分页)
|
||||
* 支持关键字搜索,排除管理员用户,清除密码信息
|
||||
*
|
||||
* @param pageDto 分页查询数据传输对象
|
||||
*
|
||||
* @return 分页用户列表响应
|
||||
*/
|
||||
@Override
|
||||
public R getAllUsers(PageDto pageDto) {
|
||||
// 1. 构建分页查询
|
||||
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);
|
||||
public R getAllUsers() {
|
||||
return R.ok(this.list(new QueryWrapper<User>().ne("role_id", ADMIN_ROLE_ID)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,39 +437,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
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/system": "2.4.19",
|
||||
"@heroui/table": "^2.2.24",
|
||||
"@heroui/tabs": "^2.2.21",
|
||||
"@heroui/theme": "2.4.19",
|
||||
"@heroui/use-theme": "2.1.10",
|
||||
"@nextui-org/system": "^2.4.6",
|
||||
|
||||
@@ -243,11 +243,11 @@ export default function AdminLayout({
|
||||
);
|
||||
|
||||
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 && (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
@@ -263,7 +263,8 @@ export default function AdminLayout({
|
||||
z-50
|
||||
transition-transform duration-300 ease-in-out
|
||||
flex flex-col
|
||||
h-full
|
||||
${isMobile ? 'h-screen' : 'h-full'}
|
||||
${isMobile ? 'top-0 left-0' : ''}
|
||||
`}>
|
||||
{/* Logo 区域 */}
|
||||
<div className="px-3 py-3 h-14 flex items-center">
|
||||
@@ -277,7 +278,7 @@ export default function AdminLayout({
|
||||
</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">
|
||||
{filteredMenuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
@@ -306,7 +307,7 @@ export default function AdminLayout({
|
||||
</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">
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||
Powered by{' '}
|
||||
@@ -324,7 +325,7 @@ export default function AdminLayout({
|
||||
</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">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -387,7 +388,7 @@ export default function AdminLayout({
|
||||
</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}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -122,6 +122,20 @@ export default function ForwardPage() {
|
||||
const [forwards, setForwards] = useState<Forward[]>([]);
|
||||
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读取,默认为分类显示
|
||||
const [viewMode, setViewMode] = useState<'grouped' | 'direct'>(() => {
|
||||
try {
|
||||
@@ -1009,12 +1023,21 @@ export default function ForwardPage() {
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
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));
|
||||
const newIndex = forwardOrder.indexOf(Number(over.id));
|
||||
// 确保 forwardOrder 存在且有效
|
||||
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);
|
||||
setForwardOrder(newOrder);
|
||||
|
||||
@@ -1052,7 +1075,7 @@ export default function ForwardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 传感器配置
|
||||
// 传感器配置 - 使用默认配置避免错误
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
@@ -1062,6 +1085,11 @@ export default function ForwardPage() {
|
||||
|
||||
// 根据排序顺序获取转发列表
|
||||
const getSortedForwards = (): Forward[] => {
|
||||
// 确保 forwards 数组存在且有效
|
||||
if (!forwards || forwards.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 在平铺模式下,只显示当前用户的转发
|
||||
let filteredForwards = forwards;
|
||||
if (viewMode === 'direct') {
|
||||
@@ -1071,6 +1099,11 @@ export default function ForwardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 确保过滤后的转发列表有效
|
||||
if (!filteredForwards || filteredForwards.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 优先使用数据库中的 inx 字段进行排序
|
||||
const sortedForwards = [...filteredForwards].sort((a, b) => {
|
||||
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 localSortedForwards: Forward[] = [];
|
||||
|
||||
@@ -1105,6 +1138,11 @@ export default function ForwardPage() {
|
||||
|
||||
// 可拖拽的转发卡片组件
|
||||
const SortableForwardCard = ({ forward }: { forward: Forward }) => {
|
||||
// 确保 forward 对象有效
|
||||
if (!forward || !forward.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -1115,20 +1153,20 @@ export default function ForwardPage() {
|
||||
} = useSortable({ id: forward.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
transform: transform ? CSS.Transform.toString(transform) : undefined,
|
||||
transition: transition || undefined,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||
{renderForwardCard(forward)}
|
||||
<div ref={setNodeRef} style={style} {...attributes}>
|
||||
{renderForwardCard(forward, listeners)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染转发卡片
|
||||
const renderForwardCard = (forward: Forward) => {
|
||||
const renderForwardCard = (forward: Forward, listeners?: any) => {
|
||||
const statusDisplay = getStatusDisplay(forward.status);
|
||||
const strategyDisplay = getStrategyDisplay(forward.strategy);
|
||||
|
||||
@@ -1142,7 +1180,16 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
{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">
|
||||
<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>
|
||||
@@ -1363,6 +1410,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 根据显示模式渲染不同内容 */}
|
||||
{viewMode === 'grouped' ? (
|
||||
/* 按用户和隧道分组的转发列表 */
|
||||
@@ -1420,7 +1468,7 @@ export default function ForwardPage() {
|
||||
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">
|
||||
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward))}
|
||||
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward, undefined))}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
))}
|
||||
@@ -1454,14 +1502,17 @@ export default function ForwardPage() {
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={() => {}} // 添加空的 onDragStart 处理器
|
||||
>
|
||||
<SortableContext
|
||||
items={getSortedForwards().map(f => f.id)}
|
||||
items={getSortedForwards().map(f => f.id || 0).filter(id => id > 0)}
|
||||
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">
|
||||
{getSortedForwards().map((forward) => (
|
||||
<SortableForwardCard key={forward.id} forward={forward} />
|
||||
forward && forward.id ? (
|
||||
<SortableForwardCard key={forward.id} forward={forward} />
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Textarea } from "@heroui/input";
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
|
||||
import { Chip } from "@heroui/chip";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Alert } from "@heroui/alert";
|
||||
import { Progress } from "@heroui/progress";
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -871,6 +872,28 @@ export default function NodePage() {
|
||||
max={65535}
|
||||
/>
|
||||
</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>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -770,12 +770,14 @@ export default function TunnelPage() {
|
||||
isInvalid={!!errors.protocol}
|
||||
errorMessage={errors.protocol}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
>
|
||||
<SelectItem key="tls">TLS</SelectItem>
|
||||
<SelectItem key="wss">WSS</SelectItem>
|
||||
<SelectItem key="tcp">TCP</SelectItem>
|
||||
<SelectItem key="mtls">mTLS</SelectItem>
|
||||
<SelectItem key="mwss">mWSS</SelectItem>
|
||||
<SelectItem key="mtcp">mTCP</SelectItem>
|
||||
<SelectItem key="quic">QUIC</SelectItem>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
|
||||
@@ -19,13 +19,12 @@ import {
|
||||
useDisclosure
|
||||
} from "@heroui/modal";
|
||||
import { Chip } from "@heroui/chip";
|
||||
import { Pagination } from "@heroui/pagination";
|
||||
import { Select, SelectItem } from "@heroui/select";
|
||||
import { RadioGroup, Radio } from "@heroui/radio";
|
||||
import { DatePicker } from "@heroui/date-picker";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Progress } from "@heroui/progress";
|
||||
import { Tabs, Tab } from "@heroui/tabs";
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
User,
|
||||
@@ -191,13 +190,7 @@ export default function UserPage() {
|
||||
|
||||
if (response.code === 0) {
|
||||
const data = response.data || {};
|
||||
setUsers(data.records || []);
|
||||
setPagination(prev => ({
|
||||
...prev,
|
||||
total: data.total || 0,
|
||||
current: data.current || 1,
|
||||
size: data.size || 10
|
||||
}));
|
||||
setUsers(data || []);
|
||||
} else {
|
||||
toast.error(response.msg || '获取用户列表失败');
|
||||
}
|
||||
@@ -748,18 +741,6 @@ export default function UserPage() {
|
||||
</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
|
||||
@@ -877,16 +858,22 @@ export default function UserPage() {
|
||||
<Modal
|
||||
isOpen={isTunnelModalOpen}
|
||||
onClose={onTunnelModalClose}
|
||||
size="5xl"
|
||||
size="4xl"
|
||||
scrollBehavior="outside"
|
||||
isDismissable={false}
|
||||
classNames={{
|
||||
base: "max-w-[95vw] sm:max-w-4xl"
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
用户 {currentUser?.user} 的隧道权限管理
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Tabs defaultSelectedKey="assign">
|
||||
<Tab key="assign" title="分配新权限">
|
||||
<div className="space-y-6">
|
||||
{/* 分配新权限部分 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">分配新权限</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Select
|
||||
@@ -991,9 +978,11 @@ export default function UserPage() {
|
||||
分配权限
|
||||
</Button>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab key="list" title="已有权限">
|
||||
</div>
|
||||
|
||||
{/* 已有权限部分 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">已有权限</h3>
|
||||
<Table
|
||||
aria-label="用户隧道权限列表"
|
||||
classNames={{
|
||||
@@ -1083,8 +1072,8 @@ export default function UserPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onTunnelModalClose}>
|
||||
@@ -1100,6 +1089,7 @@ export default function UserPage() {
|
||||
onClose={onEditTunnelModalClose}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
isDismissable={false}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
@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"
|
||||
Reference in New Issue
Block a user