mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 01: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 {
|
func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[string]interface{}) error {
|
||||||
allocated := map[int64]int{}
|
allocated := map[int64]int{}
|
||||||
inNodes := asMapSlice(req["inNodeId"])
|
inNodes := asMapSlice(req["inNodeId"])
|
||||||
for _, n := range inNodes {
|
for i, n := range inNodes {
|
||||||
nodeID := asInt64(n["nodeId"], 0)
|
nodeID := asInt64(n["nodeId"], 0)
|
||||||
if nodeID <= 0 {
|
if nodeID <= 0 {
|
||||||
continue
|
continue
|
||||||
@@ -2818,13 +2818,13 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
nodeID,
|
nodeID,
|
||||||
sql.NullInt64{},
|
sql.NullInt64{},
|
||||||
defaultString(asString(n["strategy"]), "round"),
|
defaultString(asString(n["strategy"]), "round"),
|
||||||
0,
|
i+1,
|
||||||
defaultString(asString(n["protocol"]), "tls"),
|
defaultString(asString(n["protocol"]), "tls"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, n := range asMapSlice(req["outNodeId"]) {
|
for i, n := range asMapSlice(req["outNodeId"]) {
|
||||||
nodeID := asInt64(n["nodeId"], 0)
|
nodeID := asInt64(n["nodeId"], 0)
|
||||||
if nodeID <= 0 {
|
if nodeID <= 0 {
|
||||||
continue
|
continue
|
||||||
@@ -2844,7 +2844,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
nodeID,
|
nodeID,
|
||||||
sql.NullInt64{Int64: int64(port), Valid: true},
|
sql.NullInt64{Int64: int64(port), Valid: true},
|
||||||
defaultString(asString(n["strategy"]), "round"),
|
defaultString(asString(n["strategy"]), "round"),
|
||||||
0,
|
i+1,
|
||||||
defaultString(asString(n["protocol"]), "tls"),
|
defaultString(asString(n["protocol"]), "tls"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -503,10 +503,30 @@ export default function ForwardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
const availableSpeedLimits = useMemo(() => {
|
const noLimitSpeedLimitIds = useMemo(() => {
|
||||||
return speedLimits;
|
return new Set(
|
||||||
|
speedLimits
|
||||||
|
.filter((speedLimit) => speedLimit.name.trim() === "不限速")
|
||||||
|
.map((speedLimit) => speedLimit.id),
|
||||||
|
);
|
||||||
}, [speedLimits]);
|
}, [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 validateForm = (): boolean => {
|
||||||
const newErrors: { [key: string]: string } = {};
|
const newErrors: { [key: string]: string } = {};
|
||||||
|
|
||||||
@@ -591,7 +611,7 @@ export default function ForwardPage() {
|
|||||||
remoteAddr: forward.remoteAddr.split(",").join("\n"),
|
remoteAddr: forward.remoteAddr.split(",").join("\n"),
|
||||||
interfaceName: forward.interfaceName || "",
|
interfaceName: forward.interfaceName || "",
|
||||||
strategy: forward.strategy || "fifo",
|
strategy: forward.strategy || "fifo",
|
||||||
speedId: forward.speedId ?? null,
|
speedId: normalizeSpeedId(forward.speedId),
|
||||||
});
|
});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -671,7 +691,7 @@ export default function ForwardPage() {
|
|||||||
inPort: form.inPort,
|
inPort: form.inPort,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||||
speedId: form.speedId,
|
speedId: normalizeSpeedId(form.speedId),
|
||||||
};
|
};
|
||||||
|
|
||||||
res = await updateForward(updateData);
|
res = await updateForward(updateData);
|
||||||
@@ -683,7 +703,7 @@ export default function ForwardPage() {
|
|||||||
inPort: form.inPort,
|
inPort: form.inPort,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||||
speedId: form.speedId,
|
speedId: normalizeSpeedId(form.speedId),
|
||||||
};
|
};
|
||||||
|
|
||||||
res = await createForward(createData);
|
res = await createForward(createData);
|
||||||
@@ -2331,8 +2351,8 @@ export default function ForwardPage() {
|
|||||||
label="限速规则"
|
label="限速规则"
|
||||||
placeholder="不限速"
|
placeholder="不限速"
|
||||||
selectedKeys={
|
selectedKeys={
|
||||||
form.speedId !== null && form.speedId !== undefined
|
selectedSpeedId !== null
|
||||||
? [form.speedId.toString()]
|
? [selectedSpeedId.toString()]
|
||||||
: ["null"]
|
: ["null"]
|
||||||
}
|
}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
|
|||||||
@@ -366,6 +366,21 @@ export default function TunnelPage() {
|
|||||||
return form.chainNodes || [];
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
@@ -1241,15 +1256,10 @@ export default function TunnelPage() {
|
|||||||
const selectedIds = Array.from(keys).map((key) =>
|
const selectedIds = Array.from(keys).map((key) =>
|
||||||
parseInt(key as string),
|
parseInt(key as string),
|
||||||
);
|
);
|
||||||
const newInNodeId: ChainTunnel[] = selectedIds.map(
|
const newInNodeId = mergeOrderedNodes(
|
||||||
(nodeId) => {
|
form.inNodeId,
|
||||||
// 保留已有的端口配置
|
selectedIds,
|
||||||
const existing = form.inNodeId.find(
|
(nodeId) => ({ nodeId, chainType: 1 }),
|
||||||
(ct) => ct.nodeId === nodeId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return existing || { nodeId, chainType: 1 };
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
setForm((prev) => ({ ...prev, inNodeId: newInNodeId }));
|
setForm((prev) => ({ ...prev, inNodeId: newInNodeId }));
|
||||||
@@ -1659,21 +1669,16 @@ export default function TunnelPage() {
|
|||||||
const realNodes = currentOutNodes.filter(
|
const realNodes = currentOutNodes.filter(
|
||||||
(ct) => ct.nodeId !== -1,
|
(ct) => ct.nodeId !== -1,
|
||||||
);
|
);
|
||||||
const newOutNodeId: ChainTunnel[] =
|
const newOutNodeId = mergeOrderedNodes(
|
||||||
selectedIds.map((nodeId) => {
|
realNodes,
|
||||||
const existing = realNodes.find(
|
selectedIds,
|
||||||
(ct) => ct.nodeId === nodeId,
|
(nodeId) => ({
|
||||||
);
|
nodeId,
|
||||||
|
chainType: 3,
|
||||||
return (
|
protocol,
|
||||||
existing || {
|
strategy,
|
||||||
nodeId,
|
}),
|
||||||
chainType: 3,
|
);
|
||||||
protocol,
|
|
||||||
strategy,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { parseDate } from "@internationalized/date";
|
import { parseDate } from "@internationalized/date";
|
||||||
|
|
||||||
@@ -219,6 +219,22 @@ export default function UserPage() {
|
|||||||
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
|
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
|
||||||
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
@@ -432,7 +448,10 @@ export default function UserPage() {
|
|||||||
try {
|
try {
|
||||||
const tunnelsToAssign: TunnelAssignItem[] = Array.from(
|
const tunnelsToAssign: TunnelAssignItem[] = Array.from(
|
||||||
batchTunnelSelections.entries(),
|
batchTunnelSelections.entries(),
|
||||||
).map(([tunnelId, speedId]) => ({ tunnelId, speedId }));
|
).map(([tunnelId, speedId]) => ({
|
||||||
|
tunnelId,
|
||||||
|
speedId: normalizeSpeedId(speedId),
|
||||||
|
}));
|
||||||
|
|
||||||
const response = await batchAssignUserTunnel({
|
const response = await batchAssignUserTunnel({
|
||||||
userId: currentUser.id,
|
userId: currentUser.id,
|
||||||
@@ -456,6 +475,7 @@ export default function UserPage() {
|
|||||||
const handleEditTunnel = (userTunnel: UserTunnel) => {
|
const handleEditTunnel = (userTunnel: UserTunnel) => {
|
||||||
setEditTunnelForm({
|
setEditTunnelForm({
|
||||||
...userTunnel,
|
...userTunnel,
|
||||||
|
speedId: normalizeSpeedId(userTunnel.speedId),
|
||||||
expTime: userTunnel.expTime,
|
expTime: userTunnel.expTime,
|
||||||
});
|
});
|
||||||
onEditTunnelModalOpen();
|
onEditTunnelModalOpen();
|
||||||
@@ -472,7 +492,7 @@ export default function UserPage() {
|
|||||||
num: editTunnelForm.num,
|
num: editTunnelForm.num,
|
||||||
expTime: editTunnelForm.expTime,
|
expTime: editTunnelForm.expTime,
|
||||||
flowResetTime: editTunnelForm.flowResetTime,
|
flowResetTime: editTunnelForm.flowResetTime,
|
||||||
speedId: editTunnelForm.speedId,
|
speedId: normalizeSpeedId(editTunnelForm.speedId),
|
||||||
status: editTunnelForm.status,
|
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) => {
|
const getSpeedLimitsForTunnel = (_tunnelId: number) => {
|
||||||
return speedLimits;
|
return speedLimits.filter(
|
||||||
|
(speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const editTunnelSelectedSpeedId = normalizeSpeedId(editTunnelForm?.speedId);
|
||||||
|
|
||||||
const toggleTunnelSelection = (tunnelId: number) => {
|
const toggleTunnelSelection = (tunnelId: number) => {
|
||||||
setBatchTunnelSelections((prev) => {
|
setBatchTunnelSelections((prev) => {
|
||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
@@ -1404,8 +1430,8 @@ export default function UserPage() {
|
|||||||
<Select
|
<Select
|
||||||
label="限速规则"
|
label="限速规则"
|
||||||
selectedKeys={
|
selectedKeys={
|
||||||
editTunnelForm.speedId
|
editTunnelSelectedSpeedId !== null
|
||||||
? [editTunnelForm.speedId.toString()]
|
? [editTunnelSelectedSpeedId.toString()]
|
||||||
: ["null"]
|
: ["null"]
|
||||||
}
|
}
|
||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user