mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 10:46:37 +08:00
fix(node): isolate tab search and selection state
This commit is contained in:
@@ -193,8 +193,12 @@ export default function NodePage() {
|
|||||||
const [nodeList, setNodeList] = useState<Node[]>([]);
|
const [nodeList, setNodeList] = useState<Node[]>([]);
|
||||||
const [nodeOrder, setNodeOrder] = useState<number[]>([]);
|
const [nodeOrder, setNodeOrder] = useState<number[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchKeyword, setSearchKeyword] = useLocalStorageState(
|
const [localSearchKeyword, setLocalSearchKeyword] = useLocalStorageState(
|
||||||
"node-search-keyword",
|
"node-search-keyword-local",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const [remoteSearchKeyword, setRemoteSearchKeyword] = useLocalStorageState(
|
||||||
|
"node-search-keyword-remote",
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
const [activeTab, setActiveTab] = useLocalStorageState<NodeTab>(
|
const [activeTab, setActiveTab] = useLocalStorageState<NodeTab>(
|
||||||
@@ -437,6 +441,10 @@ export default function NodePage() {
|
|||||||
loadRemoteUsage();
|
loadRemoteUsage();
|
||||||
}, [loadNodes, loadRemoteUsage]);
|
}, [loadNodes, loadRemoteUsage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
// 格式化速度
|
// 格式化速度
|
||||||
const formatSpeed = (bytesPerSecond: number): string => {
|
const formatSpeed = (bytesPerSecond: number): string => {
|
||||||
if (bytesPerSecond === 0) return "0 B/s";
|
if (bytesPerSecond === 0) return "0 B/s";
|
||||||
@@ -849,17 +857,30 @@ export default function NodePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (upgradeTarget === "batch") {
|
} else if (upgradeTarget === "batch") {
|
||||||
|
const selectedLocalIds = Array.from(selectedIds).filter((id) => {
|
||||||
|
const matchedNode = nodeList.find((node) => node.id === id);
|
||||||
|
|
||||||
|
return matchedNode?.isRemote !== 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedLocalIds.length === 0) {
|
||||||
|
toast.error("请选择本地节点进行升级");
|
||||||
|
setUpgradeModalOpen(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setBatchUpgradeLoading(true);
|
setBatchUpgradeLoading(true);
|
||||||
setUpgradeModalOpen(false);
|
setUpgradeModalOpen(false);
|
||||||
try {
|
try {
|
||||||
const res = await batchUpgradeNodes(
|
const res = await batchUpgradeNodes(
|
||||||
Array.from(selectedIds),
|
selectedLocalIds,
|
||||||
version,
|
version,
|
||||||
releaseChannel,
|
releaseChannel,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
toast.success(`批量升级命令已发送到 ${selectedIds.size} 个节点`);
|
toast.success(`批量升级命令已发送到 ${selectedLocalIds.length} 个节点`);
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg || "批量升级失败");
|
toast.error(res.msg || "批量升级失败");
|
||||||
}
|
}
|
||||||
@@ -990,12 +1011,26 @@ export default function NodePage() {
|
|||||||
|
|
||||||
if (isNaN(activeId) || isNaN(overId)) return;
|
if (isNaN(activeId) || isNaN(overId)) return;
|
||||||
|
|
||||||
const oldIndex = nodeOrder.indexOf(activeId);
|
const displayNodeIds = displayNodes.map((node) => node.id);
|
||||||
const newIndex = nodeOrder.indexOf(overId);
|
const oldIndex = displayNodeIds.indexOf(activeId);
|
||||||
|
const newIndex = displayNodeIds.indexOf(overId);
|
||||||
|
|
||||||
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return;
|
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return;
|
||||||
|
|
||||||
const newOrder = arrayMove(nodeOrder, oldIndex, newIndex);
|
const reorderedDisplayIds = arrayMove(displayNodeIds, oldIndex, newIndex);
|
||||||
|
const displayIdSet = new Set(displayNodeIds);
|
||||||
|
let reorderedDisplayIndex = 0;
|
||||||
|
|
||||||
|
const newOrder = nodeOrder.map((id) => {
|
||||||
|
if (!displayIdSet.has(id)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextId = reorderedDisplayIds[reorderedDisplayIndex];
|
||||||
|
reorderedDisplayIndex += 1;
|
||||||
|
|
||||||
|
return nextId;
|
||||||
|
});
|
||||||
|
|
||||||
setNodeOrder(newOrder);
|
setNodeOrder(newOrder);
|
||||||
|
|
||||||
@@ -1099,21 +1134,7 @@ export default function NodePage() {
|
|||||||
const sortedNodes = useMemo((): Node[] => {
|
const sortedNodes = useMemo((): Node[] => {
|
||||||
if (!nodeList || nodeList.length === 0) return [];
|
if (!nodeList || nodeList.length === 0) return [];
|
||||||
|
|
||||||
let filteredNodes = nodeList;
|
const sortedByDb = [...nodeList].sort((a, b) => {
|
||||||
|
|
||||||
if (searchKeyword.trim()) {
|
|
||||||
const lowerKeyword = searchKeyword.toLowerCase();
|
|
||||||
|
|
||||||
filteredNodes = filteredNodes.filter(
|
|
||||||
(n) =>
|
|
||||||
(n.name && n.name.toLowerCase().includes(lowerKeyword)) ||
|
|
||||||
(n.serverIp && n.serverIp.toLowerCase().includes(lowerKeyword)) ||
|
|
||||||
(n.serverIpV4 && n.serverIpV4.toLowerCase().includes(lowerKeyword)) ||
|
|
||||||
(n.serverIpV6 && n.serverIpV6.toLowerCase().includes(lowerKeyword)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedByDb = [...filteredNodes].sort((a, b) => {
|
|
||||||
const aInx = a.inx ?? 0;
|
const aInx = a.inx ?? 0;
|
||||||
const bInx = b.inx ?? 0;
|
const bInx = b.inx ?? 0;
|
||||||
|
|
||||||
@@ -1126,7 +1147,7 @@ export default function NodePage() {
|
|||||||
nodeOrder.length > 0 &&
|
nodeOrder.length > 0 &&
|
||||||
sortedByDb.every((n) => n.inx === undefined || n.inx === 0)
|
sortedByDb.every((n) => n.inx === undefined || n.inx === 0)
|
||||||
) {
|
) {
|
||||||
const nodeMap = new Map(filteredNodes.map((n) => [n.id, n] as const));
|
const nodeMap = new Map(nodeList.map((n) => [n.id, n] as const));
|
||||||
const localSorted: Node[] = [];
|
const localSorted: Node[] = [];
|
||||||
|
|
||||||
nodeOrder.forEach((id) => {
|
nodeOrder.forEach((id) => {
|
||||||
@@ -1135,7 +1156,7 @@ export default function NodePage() {
|
|||||||
if (node) localSorted.push(node);
|
if (node) localSorted.push(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
filteredNodes.forEach((node) => {
|
nodeList.forEach((node) => {
|
||||||
if (!nodeOrder.includes(node.id)) {
|
if (!nodeOrder.includes(node.id)) {
|
||||||
localSorted.push(node);
|
localSorted.push(node);
|
||||||
}
|
}
|
||||||
@@ -1145,7 +1166,26 @@ export default function NodePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sortedByDb;
|
return sortedByDb;
|
||||||
}, [nodeList, nodeOrder, searchKeyword]);
|
}, [nodeList, nodeOrder]);
|
||||||
|
|
||||||
|
const filterNodesByKeyword = useCallback((nodes: Node[], keyword: string) => {
|
||||||
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!normalizedKeyword) {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes.filter(
|
||||||
|
(node) =>
|
||||||
|
(node.name && node.name.toLowerCase().includes(normalizedKeyword)) ||
|
||||||
|
(node.serverIp &&
|
||||||
|
node.serverIp.toLowerCase().includes(normalizedKeyword)) ||
|
||||||
|
(node.serverIpV4 &&
|
||||||
|
node.serverIpV4.toLowerCase().includes(normalizedKeyword)) ||
|
||||||
|
(node.serverIpV6 &&
|
||||||
|
node.serverIpV6.toLowerCase().includes(normalizedKeyword)),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const localNodes = useMemo(
|
const localNodes = useMemo(
|
||||||
() => sortedNodes.filter((node) => node.isRemote !== 1),
|
() => sortedNodes.filter((node) => node.isRemote !== 1),
|
||||||
@@ -1157,11 +1197,30 @@ export default function NodePage() {
|
|||||||
[sortedNodes],
|
[sortedNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
const displayNodes = useMemo(
|
const filteredLocalNodes = useMemo(
|
||||||
() => (activeTab === "remote" ? remoteNodes : localNodes),
|
() => filterNodesByKeyword(localNodes, localSearchKeyword),
|
||||||
[activeTab, localNodes, remoteNodes],
|
[filterNodesByKeyword, localNodes, localSearchKeyword],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filteredRemoteNodes = useMemo(
|
||||||
|
() => filterNodesByKeyword(remoteNodes, remoteSearchKeyword),
|
||||||
|
[filterNodesByKeyword, remoteNodes, remoteSearchKeyword],
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSearchKeyword =
|
||||||
|
activeTab === "remote" ? remoteSearchKeyword : localSearchKeyword;
|
||||||
|
|
||||||
|
const setCurrentSearchKeyword =
|
||||||
|
activeTab === "remote" ? setRemoteSearchKeyword : setLocalSearchKeyword;
|
||||||
|
|
||||||
|
const displayNodes = useMemo(
|
||||||
|
() => (activeTab === "remote" ? filteredRemoteNodes : filteredLocalNodes),
|
||||||
|
[activeTab, filteredLocalNodes, filteredRemoteNodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canBatchUpgrade = activeTab === "local";
|
||||||
|
const isSearchFiltered = currentSearchKeyword.trim().length > 0;
|
||||||
|
|
||||||
const sortableNodeIds = useMemo(
|
const sortableNodeIds = useMemo(
|
||||||
() => displayNodes.map((n) => n.id),
|
() => displayNodes.map((n) => n.id),
|
||||||
[displayNodes],
|
[displayNodes],
|
||||||
@@ -1210,8 +1269,8 @@ export default function NodePage() {
|
|||||||
? "搜索远程节点名称或IP"
|
? "搜索远程节点名称或IP"
|
||||||
: "搜索本地节点名称或IP"
|
: "搜索本地节点名称或IP"
|
||||||
}
|
}
|
||||||
value={searchKeyword}
|
value={currentSearchKeyword}
|
||||||
onChange={setSearchKeyword}
|
onChange={setCurrentSearchKeyword}
|
||||||
onClose={() => setIsSearchVisible(false)}
|
onClose={() => setIsSearchVisible(false)}
|
||||||
onOpen={() => setIsSearchVisible(true)}
|
onOpen={() => setIsSearchVisible(true)}
|
||||||
/>
|
/>
|
||||||
@@ -1241,7 +1300,7 @@ export default function NodePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
isDisabled={selectedIds.size === 0}
|
isDisabled={selectedIds.size === 0 || !canBatchUpgrade}
|
||||||
isLoading={batchUpgradeLoading}
|
isLoading={batchUpgradeLoading}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -1315,9 +1374,13 @@ export default function NodePage() {
|
|||||||
<PageEmptyState
|
<PageEmptyState
|
||||||
className="h-64"
|
className="h-64"
|
||||||
message={
|
message={
|
||||||
activeTab === "remote"
|
isSearchFiltered
|
||||||
? "暂无远程节点"
|
? activeTab === "remote"
|
||||||
: "暂无本地节点,点击上方按钮开始创建"
|
? "未找到匹配的远程节点"
|
||||||
|
: "未找到匹配的本地节点"
|
||||||
|
: activeTab === "remote"
|
||||||
|
? "暂无远程节点"
|
||||||
|
: "暂无本地节点,点击上方按钮开始创建"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user