diff --git a/go-gost/gost b/go-gost/gost index 6e5ee99..edfd8df 100755 Binary files a/go-gost/gost and b/go-gost/gost differ diff --git a/gost.sql b/gost.sql index bf68c09..b52c42f 100644 --- a/gost.sql +++ b/gost.sql @@ -42,6 +42,7 @@ CREATE TABLE `forward` ( `created_time` bigint(20) NOT NULL, `updated_time` bigint(20) NOT NULL, `status` int(10) NOT NULL + `inx` int(10) NOT NULL DEFAULT 0, ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------------------------------------------- diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c07fb05 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "forward-panel", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/panel_install.sh b/panel_install.sh index a1d5e68..3c93002 100755 --- a/panel_install.sh +++ b/panel_install.sh @@ -756,6 +756,29 @@ UPDATE \`forward\` SET \`strategy\` = 'fifo' WHERE \`strategy\` IS NULL; +-- forward 表:添加 inx 字段(排序索引) +SET @sql = ( + SELECT IF( + NOT EXISTS ( + SELECT 1 + FROM information_schema.COLUMNS + WHERE table_schema = DATABASE() + AND table_name = 'forward' + AND column_name = 'inx' + ), + 'ALTER TABLE \`forward\` ADD COLUMN \`inx\` INT(10) DEFAULT 0 COMMENT "排序索引";', + 'SELECT "Column \`inx\` already exists in \`forward\`";' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 为现有数据设置默认排序索引 +UPDATE \`forward\` +SET \`inx\` = 0 +WHERE \`inx\` IS NULL; + -- 创建 vite_config 表(如果不存在) CREATE TABLE IF NOT EXISTS \`vite_config\` ( \`id\` int(10) NOT NULL AUTO_INCREMENT, diff --git a/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java b/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java index b6df8cc..073a192 100644 --- a/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java +++ b/springboot-backend/src/main/java/com/admin/common/dto/ForwardWithTunnelDto.java @@ -85,4 +85,6 @@ public class ForwardWithTunnelDto { private Long outFlow; private String strategy; + + private Integer inx; } \ No newline at end of file diff --git a/springboot-backend/src/main/java/com/admin/controller/ForwardController.java b/springboot-backend/src/main/java/com/admin/controller/ForwardController.java index bb4db25..7be4851 100644 --- a/springboot-backend/src/main/java/com/admin/controller/ForwardController.java +++ b/springboot-backend/src/main/java/com/admin/controller/ForwardController.java @@ -87,4 +87,15 @@ public class ForwardController extends BaseController { return forwardService.diagnoseForward(forwardId); } + /** + * 更新转发排序 + * @param params 包含forwards数组的参数,每个元素包含id和inx + * @return 更新结果 + */ + @LogAnnotation + @PostMapping("/update-order") + public R updateForwardOrder(@RequestBody Map params) { + return forwardService.updateForwardOrder(params); + } + } diff --git a/springboot-backend/src/main/java/com/admin/entity/Forward.java b/springboot-backend/src/main/java/com/admin/entity/Forward.java index 53f5d86..52e87df 100644 --- a/springboot-backend/src/main/java/com/admin/entity/Forward.java +++ b/springboot-backend/src/main/java/com/admin/entity/Forward.java @@ -38,4 +38,6 @@ public class Forward extends BaseEntity{ private Long outFlow; + private Integer inx; + } diff --git a/springboot-backend/src/main/java/com/admin/service/ForwardService.java b/springboot-backend/src/main/java/com/admin/service/ForwardService.java index 9be157d..d6dbdaa 100644 --- a/springboot-backend/src/main/java/com/admin/service/ForwardService.java +++ b/springboot-backend/src/main/java/com/admin/service/ForwardService.java @@ -5,6 +5,7 @@ import com.admin.common.dto.ForwardUpdateDto; import com.admin.common.lang.R; import com.admin.entity.Forward; import com.baomidou.mybatisplus.extension.service.IService; +import java.util.Map; /** *

@@ -71,4 +72,11 @@ public interface ForwardService extends IService { * @return 诊断结果 */ R diagnoseForward(Long id); + + /** + * 更新转发排序 + * @param params 包含forwards数组的参数 + * @return 更新结果 + */ + R updateForwardOrder(Map params); } diff --git a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java index a03b4af..182483d 100644 --- a/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java +++ b/springboot-backend/src/main/java/com/admin/service/impl/ForwardServiceImpl.java @@ -452,6 +452,68 @@ public class ForwardServiceImpl extends ServiceImpl impl return R.ok(diagnosisReport); } + @Override + public R updateForwardOrder(Map params) { + try { + // 1. 获取当前用户信息 + UserInfo currentUser = getCurrentUserInfo(); + + // 2. 验证参数 + if (!params.containsKey("forwards")) { + return R.err("缺少forwards参数"); + } + + @SuppressWarnings("unchecked") + List> forwardsList = (List>) params.get("forwards"); + if (forwardsList == null || forwardsList.isEmpty()) { + return R.err("forwards参数不能为空"); + } + + // 3. 验证用户权限(只能更新自己的转发) + if (currentUser.getRoleId() != ADMIN_ROLE_ID) { + // 普通用户只能更新自己的转发 + List forwardIds = forwardsList.stream() + .map(item -> Long.valueOf(item.get("id").toString())) + .collect(Collectors.toList()); + + // 检查所有转发是否属于当前用户 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("id", forwardIds); + queryWrapper.eq("user_id", currentUser.getUserId()); + + long count = this.count(queryWrapper); + if (count != forwardIds.size()) { + return R.err("只能更新自己的转发排序"); + } + } + + // 4. 批量更新排序 + List forwardsToUpdate = new ArrayList<>(); + for (Map forwardData : forwardsList) { + Long id = Long.valueOf(forwardData.get("id").toString()); + Integer inx = Integer.valueOf(forwardData.get("inx").toString()); + + Forward forward = new Forward(); + forward.setId(id); + forward.setInx(inx); + forwardsToUpdate.add(forward); + } + + // 5. 执行批量更新 + boolean success = this.updateBatchById(forwardsToUpdate); + if (success) { + log.info("用户 {} 更新了 {} 个转发的排序", currentUser.getUserName(), forwardsToUpdate.size()); + return R.ok("排序更新成功"); + } else { + return R.err("排序更新失败"); + } + + } catch (Exception e) { + log.error("更新转发排序失败", e); + return R.err("更新排序时发生错误: " + e.getMessage()); + } + } + /** * 从地址字符串中提取IP地址 * 支持格式: ip:port, [ipv6]:port, domain:port diff --git a/springboot-backend/src/main/resources/mapper/ForwardMapper.xml b/springboot-backend/src/main/resources/mapper/ForwardMapper.xml index 4192739..a536d7c 100644 --- a/springboot-backend/src/main/resources/mapper/ForwardMapper.xml +++ b/springboot-backend/src/main/resources/mapper/ForwardMapper.xml @@ -19,6 +19,7 @@ f.in_flow AS inFlow, f.strategy AS strategy, f.out_flow AS outFlow, + f.inx AS inx, t.name AS tunnelName, t.in_ip AS inIp, t.out_ip AS outIp, @@ -49,6 +50,7 @@ f.in_flow AS inFlow, f.out_flow AS outFlow, f.strategy AS strategy, + f.inx AS inx, t.name AS tunnelName, t.in_ip AS inIp, t.out_ip AS outIp, diff --git a/vite-frontend/package.json b/vite-frontend/package.json index c4410f7..f591617 100644 --- a/vite-frontend/package.json +++ b/vite-frontend/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@heroui/accordion": "^2.2.21", "@heroui/alert": "^2.2.24", "@heroui/autocomplete": "^2.3.25", @@ -44,10 +47,12 @@ "@react-types/shared": "3.30.0", "@tailwindcss/postcss": "4.1.11", "@tailwindcss/vite": "4.1.11", + "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.11.0", "clsx": "2.1.1", "framer-motion": "11.18.2", "react": "18.3.1", + "react-beautiful-dnd": "^13.1.1", "react-dom": "18.3.1", "react-hot-toast": "^2.5.2", "react-router-dom": "6.23.0", diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index 7a2886e..c2e42ce 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -63,6 +63,9 @@ export const resumeForwardService = (forwardId: number) => Network.post("/forwar // 转发诊断操作 export const diagnoseForward = (forwardId: number) => Network.post("/forward/diagnose", { forwardId }); +// 转发排序操作 +export const updateForwardOrder = (data: { forwards: Array<{ id: number; inx: number }> }) => Network.post("/forward/update-order", data); + // 限速规则CRUD操作 - 全部使用POST请求 export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data); export const getSpeedLimitList = () => Network.post("/speed-limit/list"); diff --git a/vite-frontend/src/config/site.ts b/vite-frontend/src/config/site.ts index bb693c7..a2cc832 100644 --- a/vite-frontend/src/config/site.ts +++ b/vite-frontend/src/config/site.ts @@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig; // 缓存相关常量 const CACHE_PREFIX = 'vite_config_'; -const VERSION = "1.2.2"; +const VERSION = "1.2.3"; // 缓存策略: // - 长期有效,不设置过期时间 diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index f2538ff..0a351dc 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -11,6 +11,25 @@ import { Switch } from "@heroui/switch"; import { Alert } from "@heroui/alert"; import { Accordion, AccordionItem } from "@heroui/accordion"; import toast from 'react-hot-toast'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy, +} from '@dnd-kit/sortable'; +import { + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import AdminLayout from "@/layouts/admin"; import { @@ -22,8 +41,10 @@ import { userTunnel, pauseForwardService, resumeForwardService, - diagnoseForward + diagnoseForward, + updateForwardOrder } from "@/api"; +import { JwtUtil } from "@/utils/jwt"; interface Forward { id: number; @@ -41,6 +62,7 @@ interface Forward { createdTime: string; userName?: string; userId?: number; + inx?: number; } interface Tunnel { @@ -110,6 +132,9 @@ export default function ForwardPage() { } }); + // 拖拽排序相关状态 + const [forwardOrder, setForwardOrder] = useState([]); + // 模态框状态 const [modalOpen, setModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); @@ -166,6 +191,55 @@ export default function ForwardPage() { setViewMode(newMode); try { localStorage.setItem('forward-view-mode', newMode); + + // 切换到直接显示模式时,初始化拖拽排序顺序 + if (newMode === 'direct') { + // 在平铺模式下,只对当前用户的转发进行排序 + const currentUserId = JwtUtil.getUserIdFromToken(); + let userForwards = forwards; + if (currentUserId !== null) { + userForwards = forwards.filter((f: Forward) => f.userId === currentUserId); + } + + // 检查数据库中是否有排序信息 + const hasDbOrdering = userForwards.some((f: Forward) => f.inx !== undefined && f.inx !== 0); + + if (hasDbOrdering) { + // 使用数据库中的排序信息 + const dbOrder = userForwards + .sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0)) + .map((f: Forward) => f.id); + setForwardOrder(dbOrder); + + // 同步到localStorage + try { + localStorage.setItem('forward-order', JSON.stringify(dbOrder)); + } catch (error) { + console.warn('无法保存排序到localStorage:', error); + } + } else { + // 使用本地存储的顺序 + const savedOrder = localStorage.getItem('forward-order'); + if (savedOrder) { + try { + const orderIds = JSON.parse(savedOrder); + const validOrder = orderIds.filter((id: number) => + userForwards.some((f: Forward) => f.id === id) + ); + userForwards.forEach((forward: Forward) => { + if (!validOrder.includes(forward.id)) { + validOrder.push(forward.id); + } + }); + setForwardOrder(validOrder); + } catch { + setForwardOrder(userForwards.map((f: Forward) => f.id)); + } + } else { + setForwardOrder(userForwards.map((f: Forward) => f.id)); + } + } + } } catch (error) { console.warn('无法保存显示模式到localStorage:', error); } @@ -181,10 +255,62 @@ export default function ForwardPage() { ]); if (forwardsRes.code === 0) { - setForwards(forwardsRes.data?.map((forward: any) => ({ + const forwardsData = forwardsRes.data?.map((forward: any) => ({ ...forward, serviceRunning: forward.status === 1 - })) || []); + })) || []; + setForwards(forwardsData); + + // 初始化拖拽排序顺序 + if (viewMode === 'direct') { + // 在平铺模式下,只对当前用户的转发进行排序 + const currentUserId = JwtUtil.getUserIdFromToken(); + let userForwards = forwardsData; + if (currentUserId !== null) { + userForwards = forwardsData.filter((f: Forward) => f.userId === currentUserId); + } + + // 检查数据库中是否有排序信息 + const hasDbOrdering = userForwards.some((f: Forward) => f.inx !== undefined && f.inx !== 0); + + if (hasDbOrdering) { + // 使用数据库中的排序信息 + const dbOrder = userForwards + .sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0)) + .map((f: Forward) => f.id); + setForwardOrder(dbOrder); + + // 同步到localStorage + try { + localStorage.setItem('forward-order', JSON.stringify(dbOrder)); + } catch (error) { + console.warn('无法保存排序到localStorage:', error); + } + } else { + // 使用本地存储的顺序 + const savedOrder = localStorage.getItem('forward-order'); + if (savedOrder) { + try { + const orderIds = JSON.parse(savedOrder); + // 验证保存的顺序是否仍然有效(只包含当前用户的转发) + const validOrder = orderIds.filter((id: number) => + userForwards.some((f: Forward) => f.id === id) + ); + // 添加新的转发ID(如果存在) + userForwards.forEach((forward: Forward) => { + if (!validOrder.includes(forward.id)) { + validOrder.push(forward.id); + } + }); + setForwardOrder(validOrder); + } catch { + setForwardOrder(userForwards.map((f: Forward) => f.id)); + } + } else { + setForwardOrder(userForwards.map((f: Forward) => f.id)); + } + } + } } else { toast.error(forwardsRes.msg || '获取转发列表失败'); } @@ -206,7 +332,10 @@ export default function ForwardPage() { const groupForwardsByUserAndTunnel = (): UserGroup[] => { const userMap = new Map(); - forwards.forEach(forward => { + // 获取排序后的转发列表 + const sortedForwards = getSortedForwards(); + + sortedForwards.forEach(forward => { const userKey = forward.userId ? forward.userId.toString() : 'unknown'; const userName = forward.userName || '未知用户'; @@ -687,7 +816,7 @@ export default function ForwardPage() { ); } else { // 直接显示模式下,过滤指定隧道的转发 - forwardsToExport = forwards.filter(forward => forward.tunnelId === selectedTunnelForExport); + forwardsToExport = getSortedForwards().filter(forward => forward.tunnelId === selectedTunnelForExport); } if (forwardsToExport.length === 0) { @@ -876,13 +1005,135 @@ export default function ForwardPage() { return addresses.length; }; + // 处理拖拽结束 + const handleDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + + if (!over || active.id === over.id) return; + + const oldIndex = forwardOrder.indexOf(Number(active.id)); + const newIndex = forwardOrder.indexOf(Number(over.id)); + + if (oldIndex !== -1 && newIndex !== -1) { + const newOrder = arrayMove(forwardOrder, oldIndex, newIndex); + setForwardOrder(newOrder); + + // 保存到localStorage + try { + localStorage.setItem('forward-order', JSON.stringify(newOrder)); + } catch (error) { + console.warn('无法保存排序到localStorage:', error); + } + + // 持久化到数据库 + try { + const forwardsToUpdate = newOrder.map((id, index) => ({ + id, + inx: index + })); + + const response = await updateForwardOrder({ forwards: forwardsToUpdate }); + if (response.code === 0) { + // 更新本地数据中的 inx 字段 + setForwards(prev => prev.map(forward => { + const updatedForward = forwardsToUpdate.find(f => f.id === forward.id); + if (updatedForward) { + return { ...forward, inx: updatedForward.inx }; + } + return forward; + })); + } else { + toast.error('保存排序失败:' + (response.msg || '未知错误')); + } + } catch (error) { + console.error('保存排序到数据库失败:', error); + toast.error('保存排序失败,请重试'); + } + } + }; + + // 传感器配置 + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + // 根据排序顺序获取转发列表 + const getSortedForwards = (): Forward[] => { + // 在平铺模式下,只显示当前用户的转发 + let filteredForwards = forwards; + if (viewMode === 'direct') { + const currentUserId = JwtUtil.getUserIdFromToken(); + if (currentUserId !== null) { + filteredForwards = forwards.filter(forward => forward.userId === currentUserId); + } + } + + // 优先使用数据库中的 inx 字段进行排序 + const sortedForwards = [...filteredForwards].sort((a, b) => { + const aInx = a.inx ?? 0; + const bInx = b.inx ?? 0; + return aInx - bInx; + }); + + // 如果数据库中没有排序信息,则使用本地存储的顺序 + if (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[] = []; + + forwardOrder.forEach(id => { + const forward = forwardMap.get(id); + if (forward) { + localSortedForwards.push(forward); + } + }); + + // 添加不在排序列表中的转发(新添加的) + filteredForwards.forEach(forward => { + if (!forwardOrder.includes(forward.id)) { + localSortedForwards.push(forward); + } + }); + + return localSortedForwards; + } + + return sortedForwards; + }; + + // 可拖拽的转发卡片组件 + const SortableForwardCard = ({ forward }: { forward: Forward }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: forward.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +

+ {renderForwardCard(forward)} +
+ ); + }; + // 渲染转发卡片 const renderForwardCard = (forward: Forward) => { const statusDisplay = getStatusDisplay(forward.status); const strategyDisplay = getStrategyDisplay(forward.strategy); return ( - +
@@ -890,6 +1141,13 @@ export default function ForwardPage() {

{forward.tunnelName}

+ {viewMode === 'direct' && ( +
+ + + +
+ )} {/* 页面头部 */}
-

转发管理

+
+

转发管理

+
{/* 显示模式切换按钮 */}
-
- - - - +
+ {/* 第一行:编辑和重置 */} +
+ + +
+ + {/* 第二行:权限和删除 */} +
+ + +