fix: remove @tanstack/react-virtual

This commit is contained in:
hamster1963
2026-07-13 09:50:34 +08:00
parent fc29ca7532
commit e0e90ac600
5 changed files with 25 additions and 189 deletions
-1
View File
@@ -42,7 +42,6 @@
"@tanstack/react-query": "5.101.0",
"@tanstack/react-query-devtools": "5.101.0",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "^3.14.5",
"@types/d3-geo": "3.1.0",
"@types/luxon": "3.7.1",
"class-variance-authority": "0.7.1",
-20
View File
@@ -59,9 +59,6 @@ importers:
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@tanstack/react-virtual':
specifier: ^3.14.5
version: 3.14.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@types/d3-geo':
specifier: 3.1.0
version: 3.1.0
@@ -1120,19 +1117,10 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-virtual@3.14.5':
resolution: {integrity: sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/table-core@8.21.3':
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@tanstack/virtual-core@3.17.3':
resolution: {integrity: sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==}
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@@ -3031,16 +3019,8 @@ snapshots:
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@tanstack/react-virtual@3.14.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@tanstack/virtual-core': 3.17.3
react: 19.2.7
react-dom: 19.2.7(react@19.2.7)
'@tanstack/table-core@8.21.3': {}
'@tanstack/virtual-core@3.17.3': {}
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.7
-159
View File
@@ -1,159 +0,0 @@
import { useWindowVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useState } from "react";
import ServerCard from "@/components/ServerCard";
import ServerCardInline from "@/components/ServerCardInline";
import type { NezhaServer } from "@/types/nezha-api";
type VirtualServerListProps = {
now: number;
servers: NezhaServer[];
inline: boolean;
};
const CARD_ROW_GAP = 8;
const CARD_COLUMNS_BREAKPOINT = 768;
function useCardColumnCount(enabled: boolean) {
const [columnCount, setColumnCount] = useState(() => {
if (!enabled || typeof window === "undefined") return 1;
return window.innerWidth >= CARD_COLUMNS_BREAKPOINT ? 2 : 1;
});
useEffect(() => {
if (!enabled) {
setColumnCount(1);
return;
}
const updateColumnCount = () => {
setColumnCount(window.innerWidth >= CARD_COLUMNS_BREAKPOINT ? 2 : 1);
};
updateColumnCount();
window.addEventListener("resize", updateColumnCount);
return () => {
window.removeEventListener("resize", updateColumnCount);
};
}, [enabled]);
return columnCount;
}
export default function VirtualServerList({
inline,
now,
servers,
}: VirtualServerListProps) {
if (inline) {
return <VirtualInlineServerList now={now} servers={servers} />;
}
return <VirtualCardServerList now={now} servers={servers} />;
}
function VirtualCardServerList({
now,
servers,
}: {
now: number;
servers: NezhaServer[];
}) {
const columnCount = useCardColumnCount(true);
const rowCount = Math.ceil(servers.length / columnCount);
const virtualizer = useWindowVirtualizer({
count: rowCount,
estimateSize: () => 150,
gap: CARD_ROW_GAP,
overscan: 6,
});
const virtualItems = virtualizer.getVirtualItems();
return (
<section
className="relative mt-6 server-card-list"
style={{ height: virtualizer.getTotalSize() }}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
}}
>
{virtualItems.map((virtualRow) => {
const rowStart = virtualRow.index * columnCount;
const row = servers.slice(rowStart, rowStart + columnCount);
return (
<div
className="grid grid-cols-1 gap-2 md:grid-cols-2"
data-index={virtualRow.index}
key={virtualRow.key}
ref={virtualizer.measureElement}
style={{
paddingBottom: CARD_ROW_GAP,
}}
>
{row.map((serverInfo) => (
<ServerCard
key={serverInfo.id}
now={now}
serverInfo={serverInfo}
/>
))}
</div>
);
})}
</div>
</section>
);
}
function VirtualInlineServerList({
now,
servers,
}: {
now: number;
servers: NezhaServer[];
}) {
const virtualizer = useWindowVirtualizer({
count: servers.length,
estimateSize: () => 86,
gap: CARD_ROW_GAP,
overscan: 8,
});
const virtualItems = virtualizer.getVirtualItems();
return (
<section
className="relative mt-6 overflow-x-scroll scrollbar-hidden server-inline-list"
style={{ height: virtualizer.getTotalSize() }}
>
<div
className="absolute left-0 top-0 px-px py-px flex w-full flex-col"
style={{
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
}}
>
{virtualItems.map((virtualRow) => {
const serverInfo = servers[virtualRow.index];
if (!serverInfo) return null;
return (
<div
data-index={virtualRow.index}
key={virtualRow.key}
ref={virtualizer.measureElement}
style={{ paddingBottom: CARD_ROW_GAP }}
>
<ServerCardInline now={now} serverInfo={serverInfo} />
</div>
);
})}
</div>
</section>
);
}
+23 -6
View File
@@ -13,9 +13,10 @@ import { useTranslation } from "react-i18next";
import GlobalMap from "@/components/GlobalMap";
import GroupSwitch from "@/components/GroupSwitch";
import { Loader } from "@/components/loading/Loader";
import ServerCard from "@/components/ServerCard";
import ServerCardInline from "@/components/ServerCardInline";
import ServerOverview from "@/components/ServerOverview";
import { ServiceTracker } from "@/components/ServiceTracker";
import VirtualServerList from "@/components/VirtualServerList";
import { SORT_TYPES } from "@/context/sort-context";
import { useSort } from "@/hooks/use-sort";
import { useStatus } from "@/hooks/use-status";
@@ -580,11 +581,27 @@ export default function Servers({
{!hasServers ? (
<ServerEmptyState />
) : filteredServers.length > 0 ? (
<VirtualServerList
inline={inline === "1"}
now={nezhaWsData.now}
servers={filteredServers}
/>
inline === "1" ? (
<section className="flex flex-col gap-2 overflow-x-scroll p-px scrollbar-hidden mt-6 server-inline-list">
{filteredServers.map((serverInfo) => (
<ServerCardInline
key={serverInfo.id}
now={nezhaWsData.now}
serverInfo={serverInfo}
/>
))}
</section>
) : (
<section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list">
{filteredServers.map((serverInfo) => (
<ServerCard
key={serverInfo.id}
now={nezhaWsData.now}
serverInfo={serverInfo}
/>
))}
</section>
)
) : (
<ServerEmptyState filtered />
)}
+2 -3
View File
@@ -424,7 +424,7 @@ describe("Servers page", () => {
});
});
it("virtualizes large server lists instead of rendering every card", () => {
it("renders every card in large server lists without virtualization", () => {
const servers = Array.from({ length: 2000 }, (_, index) =>
createServer({ id: index + 1, name: `server-${index + 1}` }),
);
@@ -437,8 +437,7 @@ describe("Servers page", () => {
expect(screen.getByTestId("server-overview")).toHaveTextContent(
"2000:2000:0",
);
expect(screen.getAllByTestId("server-card").length).toBeLessThan(2000);
expect(screen.getAllByTestId("server-card").length).toBeGreaterThan(0);
expect(screen.getAllByTestId("server-card")).toHaveLength(2000);
});
it("toggles map and service tracker controls when service data exists", async () => {