修复bug

This commit is contained in:
qaq
2025-08-06 15:00:39 +08:00
parent 496e00ec2a
commit b567628700
26 changed files with 258 additions and 180 deletions
+1 -1
View File
@@ -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) 两个开源库,实现了转发面板。
--- ---
BIN
View File
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
View File
@@ -1 +1 @@
VITE_API_BASE=http://127.0.0.1:6365 VITE_API_BASE=http://192.168.100.8:6365
-1
View File
@@ -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",
+8 -7
View File
@@ -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>
+66 -15
View File
@@ -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>
+23
View File
@@ -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>
+3 -1
View File
@@ -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 -29
View File
@@ -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>
+30
View File
@@ -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"