mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 04:53:44 +08:00
feat: Enhance alphabet selector with dynamic tooltip and scrolling
This commit is contained in:
parent
6763537f22
commit
d29fe4cb6c
@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { ScrollTopButton } from "../layout/scroll-top-button";
|
import { ScrollTopButton } from "../layout/scroll-top-button";
|
||||||
import { Box, styled } from "@mui/material";
|
import { Box, styled } from "@mui/material";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
// 将选择器组件抽离出来,避免主组件重渲染时重复创建样式
|
// 将选择器组件抽离出来,避免主组件重渲染时重复创建样式
|
||||||
const AlphabetSelector = styled(Box)(({ theme }) => ({
|
const AlphabetSelector = styled(Box)(({ theme }) => ({
|
||||||
@ -30,10 +31,25 @@ const AlphabetSelector = styled(Box)(({ theme }) => ({
|
|||||||
background: "transparent",
|
background: "transparent",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
gap: "2px",
|
gap: "2px",
|
||||||
padding: "8px 4px",
|
// padding: "4px 2px",
|
||||||
willChange: "transform", // 优化动画性能
|
willChange: "transform",
|
||||||
|
"&:hover": {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
boxShadow: theme.shadows[2],
|
||||||
|
borderRadius: "8px",
|
||||||
|
},
|
||||||
|
"& .scroll-container": {
|
||||||
|
overflow: "hidden",
|
||||||
|
maxHeight: "inherit",
|
||||||
|
},
|
||||||
|
"& .letter-container": {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "2px",
|
||||||
|
transition: "transform 0.2s ease",
|
||||||
|
},
|
||||||
"& .letter": {
|
"& .letter": {
|
||||||
padding: "2px 4px",
|
padding: "1px 4px",
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
fontFamily:
|
fontFamily:
|
||||||
@ -45,52 +61,40 @@ const AlphabetSelector = styled(Box)(({ theme }) => ({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
transition: "all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1)", // 稍微加快动画速度
|
transition: "all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
||||||
transform: "scale(1) translateZ(0)", // 开启GPU加速
|
transform: "scale(1) translateZ(0)",
|
||||||
backfaceVisibility: "hidden", // 防止闪烁
|
backfaceVisibility: "hidden",
|
||||||
borderRadius: "6px",
|
borderRadius: "6px",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
transform: "scale(1.2) translateZ(0)",
|
transform: "scale(1.4) translateZ(0)",
|
||||||
backgroundColor: theme.palette.action.hover,
|
backgroundColor: theme.palette.action.hover,
|
||||||
"& .tooltip": {
|
|
||||||
opacity: 1,
|
|
||||||
transform: "translateX(0) translateZ(0)",
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"&:hover ~ .letter": {
|
|
||||||
transform: "translateY(2px) translateZ(0)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"& .tooltip": {
|
}));
|
||||||
|
|
||||||
|
// 创建一个单独的 Tooltip 组件
|
||||||
|
const Tooltip = styled("div")(({ theme }) => ({
|
||||||
|
position: "fixed",
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
boxShadow: theme.shadows[3],
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
pointerEvents: "none",
|
||||||
|
"&::after": {
|
||||||
|
content: '""',
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: "calc(100% + 8px)",
|
right: "-4px",
|
||||||
background: theme.palette.background.paper,
|
top: "50%",
|
||||||
padding: "4px 8px",
|
transform: "translateY(-50%)",
|
||||||
borderRadius: "6px",
|
width: 0,
|
||||||
boxShadow: theme.shadows[3],
|
height: 0,
|
||||||
whiteSpace: "nowrap",
|
borderTop: "4px solid transparent",
|
||||||
opacity: 0,
|
borderBottom: "4px solid transparent",
|
||||||
visibility: "hidden",
|
borderLeft: `4px solid ${theme.palette.background.paper}`,
|
||||||
transform: "translateX(4px) translateZ(0)",
|
|
||||||
transition: "all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
||||||
fontSize: "12px",
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
pointerEvents: "none",
|
|
||||||
backfaceVisibility: "hidden",
|
|
||||||
"&::after": {
|
|
||||||
content: '""',
|
|
||||||
position: "absolute",
|
|
||||||
right: "-4px",
|
|
||||||
top: "50%",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
borderTop: "4px solid transparent",
|
|
||||||
borderBottom: "4px solid transparent",
|
|
||||||
borderLeft: `4px solid ${theme.palette.background.paper}`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -104,12 +108,56 @@ const LetterItem = memo(
|
|||||||
name: string;
|
name: string;
|
||||||
onClick: (name: string) => void;
|
onClick: (name: string) => void;
|
||||||
getFirstChar: (str: string) => string;
|
getFirstChar: (str: string) => string;
|
||||||
}) => (
|
}) => {
|
||||||
<div className="letter" onClick={() => onClick(name)}>
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
<span>{getFirstChar(name)}</span>
|
const letterRef = useRef<HTMLDivElement>(null);
|
||||||
<div className="tooltip">{name}</div>
|
const [tooltipPosition, setTooltipPosition] = useState({
|
||||||
</div>
|
top: 0,
|
||||||
),
|
right: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTooltipPosition = useCallback(() => {
|
||||||
|
if (!letterRef.current) return;
|
||||||
|
const rect = letterRef.current.getBoundingClientRect();
|
||||||
|
setTooltipPosition({
|
||||||
|
top: rect.top + rect.height / 2,
|
||||||
|
right: window.innerWidth - rect.left + 8,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showTooltip) {
|
||||||
|
updateTooltipPosition();
|
||||||
|
}
|
||||||
|
}, [showTooltip, updateTooltipPosition]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={letterRef}
|
||||||
|
className="letter"
|
||||||
|
onClick={() => onClick(name)}
|
||||||
|
onMouseEnter={() => setShowTooltip(true)}
|
||||||
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
|
>
|
||||||
|
<span>{getFirstChar(name)}</span>
|
||||||
|
</div>
|
||||||
|
{showTooltip &&
|
||||||
|
createPortal(
|
||||||
|
<Tooltip
|
||||||
|
style={{
|
||||||
|
top: tooltipPosition.top,
|
||||||
|
right: tooltipPosition.right,
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Tooltip>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -130,6 +178,9 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
const scrollPositionRef = useRef<Record<string, number>>({});
|
const scrollPositionRef = useRef<Record<string, number>>({});
|
||||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||||
const scrollerRef = useRef<Element | null>(null);
|
const scrollerRef = useRef<Element | null>(null);
|
||||||
|
const letterContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const alphabetSelectorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [maxHeight, setMaxHeight] = useState("auto");
|
||||||
|
|
||||||
// 使用useMemo缓存字母索引数据
|
// 使用useMemo缓存字母索引数据
|
||||||
const { groupFirstLetters, letterIndexMap } = useMemo(() => {
|
const { groupFirstLetters, letterIndexMap } = useMemo(() => {
|
||||||
@ -332,6 +383,64 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加滚轮事件处理函数
|
||||||
|
const handleWheel = useCallback((e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!letterContainerRef.current) return;
|
||||||
|
|
||||||
|
const container = letterContainerRef.current;
|
||||||
|
const scrollAmount = e.deltaY;
|
||||||
|
const currentTransform = new WebKitCSSMatrix(container.style.transform);
|
||||||
|
const currentY = currentTransform.m42 || 0;
|
||||||
|
|
||||||
|
const containerHeight = container.getBoundingClientRect().height;
|
||||||
|
const parentHeight =
|
||||||
|
container.parentElement?.getBoundingClientRect().height || 0;
|
||||||
|
const maxScroll = Math.max(0, containerHeight - parentHeight);
|
||||||
|
|
||||||
|
let newY = currentY - scrollAmount;
|
||||||
|
newY = Math.min(0, Math.max(-maxScroll, newY));
|
||||||
|
|
||||||
|
container.style.transform = `translateY(${newY}px)`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 添加和移除滚轮事件监听
|
||||||
|
useEffect(() => {
|
||||||
|
const container = letterContainerRef.current?.parentElement;
|
||||||
|
if (container) {
|
||||||
|
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("wheel", handleWheel);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [handleWheel]);
|
||||||
|
|
||||||
|
// 添加窗口大小变化监听和最大高度计算
|
||||||
|
const updateMaxHeight = useCallback(() => {
|
||||||
|
if (!alphabetSelectorRef.current) return;
|
||||||
|
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const bottomMargin = 60; // 底部边距
|
||||||
|
const topMargin = bottomMargin * 2; // 顶部边距是底部的2倍
|
||||||
|
const availableHeight = windowHeight - (topMargin + bottomMargin);
|
||||||
|
|
||||||
|
// 调整选择器的位置,使其偏下
|
||||||
|
const offsetPercentage =
|
||||||
|
(((topMargin - bottomMargin) / windowHeight) * 100) / 2;
|
||||||
|
alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`;
|
||||||
|
|
||||||
|
setMaxHeight(`${availableHeight}px`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
useEffect(() => {
|
||||||
|
updateMaxHeight();
|
||||||
|
window.addEventListener("resize", updateMaxHeight);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", updateMaxHeight);
|
||||||
|
};
|
||||||
|
}, [updateMaxHeight]);
|
||||||
|
|
||||||
if (mode === "direct") {
|
if (mode === "direct") {
|
||||||
return <BaseEmpty text={t("clash_mode_direct")} />;
|
return <BaseEmpty text={t("clash_mode_direct")} />;
|
||||||
}
|
}
|
||||||
@ -365,15 +474,19 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||||
|
|
||||||
<AlphabetSelector>
|
<AlphabetSelector ref={alphabetSelectorRef} style={{ maxHeight }}>
|
||||||
{groupFirstLetters.map((name) => (
|
<div className="scroll-container">
|
||||||
<LetterItem
|
<div ref={letterContainerRef} className="letter-container">
|
||||||
key={name}
|
{groupFirstLetters.map((name) => (
|
||||||
name={name}
|
<LetterItem
|
||||||
onClick={handleLetterClick}
|
key={name}
|
||||||
getFirstChar={getFirstChar}
|
name={name}
|
||||||
/>
|
onClick={handleLetterClick}
|
||||||
))}
|
getFirstChar={getFirstChar}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AlphabetSelector>
|
</AlphabetSelector>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user