diff --git a/src/components/layout/scroll-top-button.tsx b/src/components/layout/scroll-top-button.tsx new file mode 100644 index 00000000..49b8c1d3 --- /dev/null +++ b/src/components/layout/scroll-top-button.tsx @@ -0,0 +1,35 @@ +import { IconButton, Fade } from "@mui/material"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; + +interface Props { + onClick: () => void; + show: boolean; +} + +export const ScrollTopButton = ({ onClick, show }: Props) => { + return ( + + + theme.palette.mode === "dark" + ? "rgba(255,255,255,0.1)" + : "rgba(0,0,0,0.1)", + "&:hover": { + backgroundColor: (theme) => + theme.palette.mode === "dark" + ? "rgba(255,255,255,0.2)" + : "rgba(0,0,0,0.2)", + }, + visibility: show ? "visible" : "hidden", + }} + > + + + + ); +}; diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index bbe550a6..9d9af84c 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useRef, useState, useEffect } from "react"; import { useLockFn } from "ahooks"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { @@ -15,6 +15,7 @@ import { useRenderList } from "./use-render-list"; import { ProxyRender } from "./proxy-render"; import delayManager from "@/services/delay"; import { useTranslation } from "react-i18next"; +import { ScrollTopButton } from "../layout/scroll-top-button"; interface Props { mode: string; @@ -32,6 +33,22 @@ export const ProxyGroups = (props: Props) => { const virtuosoRef = useRef(null); + const [showScrollTop, setShowScrollTop] = useState(false); + + // 添加滚动处理函数 + const handleScroll = (e: any) => { + const scrollTop = e.target.scrollTop; + setShowScrollTop(scrollTop > 100); + }; + + // 滚动到顶部 + const scrollToTop = () => { + virtuosoRef.current?.scrollTo?.({ + top: 0, + behavior: "smooth", + }); + }; + // 切换分组的节点代理 const handleChangeProxy = useLockFn( async (group: IProxyGroupItem, proxy: IProxyItem) => { @@ -57,7 +74,7 @@ export const ProxyGroups = (props: Props) => { if (!current.selected) current.selected = []; const index = current.selected.findIndex( - (item) => item.name === group.name + (item) => item.name === group.name, ); if (index < 0) { @@ -66,14 +83,14 @@ export const ProxyGroups = (props: Props) => { current.selected[index] = { name, now: proxy.name }; } await patchCurrent({ selected: current.selected }); - } + }, ); // 测全部延迟 const handleCheckAll = useLockFn(async (groupName: string) => { const proxies = renderList .filter( - (e) => e.group?.name === groupName && (e.type === 2 || e.type === 4) + (e) => e.group?.name === groupName && (e.type === 2 || e.type === 4), ) .flatMap((e) => e.proxyCol || e.proxy!) .filter(Boolean); @@ -82,7 +99,7 @@ export const ProxyGroups = (props: Props) => { if (providers.size) { Promise.allSettled( - [...providers].map((p) => providerHealthCheck(p)) + [...providers].map((p) => providerHealthCheck(p)), ).then(() => onProxies()); } @@ -105,7 +122,7 @@ export const ProxyGroups = (props: Props) => { (e) => e.group?.name === name && ((e.type === 2 && e.proxy?.name === now) || - (e.type === 4 && e.proxyCol?.some((p) => p.name === now))) + (e.type === 4 && e.proxyCol?.some((p) => p.name === now))), ); if (index >= 0) { @@ -122,22 +139,33 @@ export const ProxyGroups = (props: Props) => { } return ( - ( - - )} - /> +
+ { + if (ref) { + ref.addEventListener("scroll", handleScroll); + } + }} + itemContent={(index) => ( + <> + + + )} + /> + + +
); }; diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index eb4f26ba..7dece0ea 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -1,7 +1,7 @@ import useSWR from "swr"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { Virtuoso } from "react-virtuoso"; +import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; import { Box } from "@mui/material"; import { getRules } from "@/services/api"; import { BaseEmpty, BasePage } from "@/components/base"; @@ -9,6 +9,7 @@ import RuleItem from "@/components/rule/rule-item"; import { ProviderButton } from "@/components/rule/provider-button"; import { BaseSearchBox } from "@/components/base/base-search-box"; import { useTheme } from "@mui/material/styles"; +import { ScrollTopButton } from "@/components/layout/scroll-top-button"; const RulesPage = () => { const { t } = useTranslation(); @@ -16,11 +17,24 @@ const RulesPage = () => { const theme = useTheme(); const isDark = theme.palette.mode === "dark"; const [match, setMatch] = useState(() => (_: string) => true); + const virtuosoRef = useRef(null); + const [showScrollTop, setShowScrollTop] = useState(false); const rules = useMemo(() => { return data.filter((item) => match(item.payload)); }, [data, match]); + const scrollToTop = () => { + virtuosoRef.current?.scrollTo({ + top: 0, + behavior: "smooth", + }); + }; + + const handleScroll = (e: any) => { + setShowScrollTop(e.target.scrollTop > 100); + }; + return ( { margin: "10px", borderRadius: "8px", bgcolor: isDark ? "#282a36" : "#ffffff", + position: "relative", }} > {rules.length > 0 ? ( - ( - - )} - followOutput={"smooth"} - /> + <> + ( + + )} + followOutput={"smooth"} + scrollerRef={(ref) => { + if (ref) ref.addEventListener("scroll", handleScroll); + }} + /> + + ) : ( )}