mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
fix: tunnel chain order and speed limit UI improvements (#216)
* fix(backend): use correct chain order index for tunnel nodes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(frontend): filter '不限速' from speed limit dropdowns and preserve node order Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -2806,7 +2806,7 @@ func pickNodeAddressV6(node *nodeRecord) string {
|
||||
func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[string]interface{}) error {
|
||||
allocated := map[int64]int{}
|
||||
inNodes := asMapSlice(req["inNodeId"])
|
||||
for _, n := range inNodes {
|
||||
for i, n := range inNodes {
|
||||
nodeID := asInt64(n["nodeId"], 0)
|
||||
if nodeID <= 0 {
|
||||
continue
|
||||
@@ -2818,13 +2818,13 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
||||
nodeID,
|
||||
sql.NullInt64{},
|
||||
defaultString(asString(n["strategy"]), "round"),
|
||||
0,
|
||||
i+1,
|
||||
defaultString(asString(n["protocol"]), "tls"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, n := range asMapSlice(req["outNodeId"]) {
|
||||
for i, n := range asMapSlice(req["outNodeId"]) {
|
||||
nodeID := asInt64(n["nodeId"], 0)
|
||||
if nodeID <= 0 {
|
||||
continue
|
||||
@@ -2844,7 +2844,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
||||
nodeID,
|
||||
sql.NullInt64{Int64: int64(port), Valid: true},
|
||||
defaultString(asString(n["strategy"]), "round"),
|
||||
0,
|
||||
i+1,
|
||||
defaultString(asString(n["protocol"]), "tls"),
|
||||
); err != nil {
|
||||
return err
|
||||
|
||||
@@ -503,10 +503,30 @@ export default function ForwardPage() {
|
||||
};
|
||||
|
||||
// 表单验证
|
||||
const availableSpeedLimits = useMemo(() => {
|
||||
return speedLimits;
|
||||
const noLimitSpeedLimitIds = useMemo(() => {
|
||||
return new Set(
|
||||
speedLimits
|
||||
.filter((speedLimit) => speedLimit.name.trim() === "不限速")
|
||||
.map((speedLimit) => speedLimit.id),
|
||||
);
|
||||
}, [speedLimits]);
|
||||
|
||||
const availableSpeedLimits = useMemo(() => {
|
||||
return speedLimits.filter(
|
||||
(speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),
|
||||
);
|
||||
}, [speedLimits, noLimitSpeedLimitIds]);
|
||||
|
||||
const normalizeSpeedId = (speedId?: number | null): number | null => {
|
||||
if (speedId === null || speedId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return noLimitSpeedLimitIds.has(speedId) ? null : speedId;
|
||||
};
|
||||
|
||||
const selectedSpeedId = normalizeSpeedId(form.speedId);
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
|
||||
@@ -591,7 +611,7 @@ export default function ForwardPage() {
|
||||
remoteAddr: forward.remoteAddr.split(",").join("\n"),
|
||||
interfaceName: forward.interfaceName || "",
|
||||
strategy: forward.strategy || "fifo",
|
||||
speedId: forward.speedId ?? null,
|
||||
speedId: normalizeSpeedId(forward.speedId),
|
||||
});
|
||||
setErrors({});
|
||||
setModalOpen(true);
|
||||
@@ -671,7 +691,7 @@ export default function ForwardPage() {
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||
speedId: form.speedId,
|
||||
speedId: normalizeSpeedId(form.speedId),
|
||||
};
|
||||
|
||||
res = await updateForward(updateData);
|
||||
@@ -683,7 +703,7 @@ export default function ForwardPage() {
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||
speedId: form.speedId,
|
||||
speedId: normalizeSpeedId(form.speedId),
|
||||
};
|
||||
|
||||
res = await createForward(createData);
|
||||
@@ -2331,8 +2351,8 @@ export default function ForwardPage() {
|
||||
label="限速规则"
|
||||
placeholder="不限速"
|
||||
selectedKeys={
|
||||
form.speedId !== null && form.speedId !== undefined
|
||||
? [form.speedId.toString()]
|
||||
selectedSpeedId !== null
|
||||
? [selectedSpeedId.toString()]
|
||||
: ["null"]
|
||||
}
|
||||
variant="bordered"
|
||||
|
||||
@@ -366,6 +366,21 @@ export default function TunnelPage() {
|
||||
return form.chainNodes || [];
|
||||
};
|
||||
|
||||
const mergeOrderedNodes = (
|
||||
currentNodes: ChainTunnel[],
|
||||
selectedNodeIds: number[],
|
||||
buildDefault: (nodeId: number) => ChainTunnel,
|
||||
): ChainTunnel[] => {
|
||||
const selectedSet = new Set(selectedNodeIds);
|
||||
const kept = currentNodes.filter((node) => selectedSet.has(node.nodeId));
|
||||
const keptIds = new Set(kept.map((node) => node.nodeId));
|
||||
const added = selectedNodeIds
|
||||
.filter((nodeId) => !keptIds.has(nodeId))
|
||||
.map((nodeId) => buildDefault(nodeId));
|
||||
|
||||
return [...kept, ...added];
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
@@ -1241,15 +1256,10 @@ export default function TunnelPage() {
|
||||
const selectedIds = Array.from(keys).map((key) =>
|
||||
parseInt(key as string),
|
||||
);
|
||||
const newInNodeId: ChainTunnel[] = selectedIds.map(
|
||||
(nodeId) => {
|
||||
// 保留已有的端口配置
|
||||
const existing = form.inNodeId.find(
|
||||
(ct) => ct.nodeId === nodeId,
|
||||
);
|
||||
|
||||
return existing || { nodeId, chainType: 1 };
|
||||
},
|
||||
const newInNodeId = mergeOrderedNodes(
|
||||
form.inNodeId,
|
||||
selectedIds,
|
||||
(nodeId) => ({ nodeId, chainType: 1 }),
|
||||
);
|
||||
|
||||
setForm((prev) => ({ ...prev, inNodeId: newInNodeId }));
|
||||
@@ -1659,21 +1669,16 @@ export default function TunnelPage() {
|
||||
const realNodes = currentOutNodes.filter(
|
||||
(ct) => ct.nodeId !== -1,
|
||||
);
|
||||
const newOutNodeId: ChainTunnel[] =
|
||||
selectedIds.map((nodeId) => {
|
||||
const existing = realNodes.find(
|
||||
(ct) => ct.nodeId === nodeId,
|
||||
);
|
||||
|
||||
return (
|
||||
existing || {
|
||||
nodeId,
|
||||
chainType: 3,
|
||||
protocol,
|
||||
strategy,
|
||||
}
|
||||
);
|
||||
});
|
||||
const newOutNodeId = mergeOrderedNodes(
|
||||
realNodes,
|
||||
selectedIds,
|
||||
(nodeId) => ({
|
||||
nodeId,
|
||||
chainType: 3,
|
||||
protocol,
|
||||
strategy,
|
||||
}),
|
||||
);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { parseDate } from "@internationalized/date";
|
||||
|
||||
@@ -219,6 +219,22 @@ export default function UserPage() {
|
||||
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
|
||||
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
|
||||
|
||||
const noLimitSpeedLimitIds = useMemo(() => {
|
||||
return new Set(
|
||||
speedLimits
|
||||
.filter((speedLimit) => speedLimit.name.trim() === "不限速")
|
||||
.map((speedLimit) => speedLimit.id),
|
||||
);
|
||||
}, [speedLimits]);
|
||||
|
||||
const normalizeSpeedId = (speedId?: number | null): number | null => {
|
||||
if (speedId === null || speedId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return noLimitSpeedLimitIds.has(speedId) ? null : speedId;
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
@@ -432,7 +448,10 @@ export default function UserPage() {
|
||||
try {
|
||||
const tunnelsToAssign: TunnelAssignItem[] = Array.from(
|
||||
batchTunnelSelections.entries(),
|
||||
).map(([tunnelId, speedId]) => ({ tunnelId, speedId }));
|
||||
).map(([tunnelId, speedId]) => ({
|
||||
tunnelId,
|
||||
speedId: normalizeSpeedId(speedId),
|
||||
}));
|
||||
|
||||
const response = await batchAssignUserTunnel({
|
||||
userId: currentUser.id,
|
||||
@@ -456,6 +475,7 @@ export default function UserPage() {
|
||||
const handleEditTunnel = (userTunnel: UserTunnel) => {
|
||||
setEditTunnelForm({
|
||||
...userTunnel,
|
||||
speedId: normalizeSpeedId(userTunnel.speedId),
|
||||
expTime: userTunnel.expTime,
|
||||
});
|
||||
onEditTunnelModalOpen();
|
||||
@@ -472,7 +492,7 @@ export default function UserPage() {
|
||||
num: editTunnelForm.num,
|
||||
expTime: editTunnelForm.expTime,
|
||||
flowResetTime: editTunnelForm.flowResetTime,
|
||||
speedId: editTunnelForm.speedId,
|
||||
speedId: normalizeSpeedId(editTunnelForm.speedId),
|
||||
status: editTunnelForm.status,
|
||||
});
|
||||
|
||||
@@ -582,12 +602,18 @@ export default function UserPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const editAvailableSpeedLimits = speedLimits;
|
||||
const editAvailableSpeedLimits = speedLimits.filter(
|
||||
(speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),
|
||||
);
|
||||
|
||||
const getSpeedLimitsForTunnel = (_tunnelId: number) => {
|
||||
return speedLimits;
|
||||
return speedLimits.filter(
|
||||
(speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),
|
||||
);
|
||||
};
|
||||
|
||||
const editTunnelSelectedSpeedId = normalizeSpeedId(editTunnelForm?.speedId);
|
||||
|
||||
const toggleTunnelSelection = (tunnelId: number) => {
|
||||
setBatchTunnelSelections((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
@@ -1404,8 +1430,8 @@ export default function UserPage() {
|
||||
<Select
|
||||
label="限速规则"
|
||||
selectedKeys={
|
||||
editTunnelForm.speedId
|
||||
? [editTunnelForm.speedId.toString()]
|
||||
editTunnelSelectedSpeedId !== null
|
||||
? [editTunnelSelectedSpeedId.toString()]
|
||||
: ["null"]
|
||||
}
|
||||
onSelectionChange={(keys) => {
|
||||
|
||||
Reference in New Issue
Block a user