195 lines
5.1 KiB
TypeScript
195 lines
5.1 KiB
TypeScript
import React, { useState, useRef } from "react";
|
||
import { FloatingBubble, Image } from "antd-mobile";
|
||
import {
|
||
AddOutline,
|
||
MessageOutline,
|
||
UserOutline,
|
||
SetOutline,
|
||
HeartOutline,
|
||
CheckOutline,
|
||
} from "antd-mobile-icons";
|
||
import { createPortal } from "react-dom";
|
||
|
||
import "./index.less";
|
||
import { MoreTwo } from "@icon-park/react";
|
||
|
||
export interface FloatMenuItemConfig {
|
||
icon: React.ReactNode;
|
||
type?: string;
|
||
}
|
||
|
||
const FloatingFanMenu: React.FC<{
|
||
menuItems: FloatMenuItemConfig[];
|
||
value?: FloatMenuItemConfig;
|
||
onChange?: (item: FloatMenuItemConfig) => void;
|
||
}> = (props) => {
|
||
const { menuItems = [] } = props;
|
||
const [visible, setVisible] = useState(false);
|
||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||
const bubbleRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 点击时获取FloatingBubble的位置
|
||
const handleMainClick = () => {
|
||
if (!visible) {
|
||
// 显示菜单时,获取当前FloatingBubble的位置
|
||
if (bubbleRef.current) {
|
||
const bubble = bubbleRef.current.querySelector(
|
||
".adm-floating-bubble-button"
|
||
);
|
||
if (bubble) {
|
||
const rect = bubble.getBoundingClientRect();
|
||
setMenuPosition({
|
||
x: rect.left + rect.width / 2,
|
||
y: rect.top + rect.height / 2,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
setVisible(!visible);
|
||
};
|
||
|
||
const handleItemClick = (item: FloatMenuItemConfig) => {
|
||
props.onChange?.(item);
|
||
setVisible(false);
|
||
};
|
||
|
||
// 计算菜单项位置
|
||
const getMenuItemPosition = (index: number) => {
|
||
const positions = [
|
||
{ x: 0, y: -80 }, // 上方
|
||
{ x: -60, y: -60 }, // 左上
|
||
{ x: -80, y: 0 }, // 左方
|
||
{ x: -60, y: 60 }, // 左下
|
||
];
|
||
|
||
const pos = positions[index] || { x: 0, y: 0 };
|
||
let x = menuPosition.x + pos.x;
|
||
let y = menuPosition.y + pos.y;
|
||
|
||
// // 边界检测
|
||
// const itemSize = 48;
|
||
// const margin = 20;
|
||
// 边界检测
|
||
const itemSize = 48;
|
||
// const margin = 0;
|
||
|
||
// x = Math.max(
|
||
// // margin + itemSize / 2,
|
||
// Math.min(window.innerWidth - margin - itemSize / 2, x)
|
||
// );
|
||
// y = Math.max(
|
||
// // margin + itemSize / 2,
|
||
// Math.min(window.innerHeight - margin - itemSize / 2, y)
|
||
// );
|
||
|
||
return {
|
||
left: x - itemSize / 2,
|
||
top: y - itemSize / 2,
|
||
};
|
||
};
|
||
|
||
// 菜单组件
|
||
const MenuComponent = () => (
|
||
<>
|
||
{/* 背景遮罩 */}
|
||
<div
|
||
style={{
|
||
position: "fixed",
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||
zIndex: 9998,
|
||
}}
|
||
onClick={() => setVisible(false)}
|
||
/>
|
||
|
||
{/* 菜单项 */}
|
||
{menuItems.map((item, index) => {
|
||
const position = getMenuItemPosition(index);
|
||
return (
|
||
<div
|
||
key={index}
|
||
style={{
|
||
position: "fixed",
|
||
...position,
|
||
width: 48,
|
||
height: 48,
|
||
borderRadius: "50%",
|
||
backgroundColor: "#fff",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
color: "white",
|
||
fontSize: 20,
|
||
cursor: "pointer",
|
||
zIndex: 9999,
|
||
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.2)",
|
||
animation: `menuItemPop 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards`,
|
||
animationDelay: `${index * 0.1}s`,
|
||
opacity: 0,
|
||
transform: "scale(0)",
|
||
}}
|
||
onClick={() => handleItemClick(item)}
|
||
// title={item.label}
|
||
>
|
||
{item.icon}
|
||
</div>
|
||
);
|
||
})}
|
||
</>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
{/* 主按钮 */}
|
||
<div ref={bubbleRef}>
|
||
<FloatingBubble
|
||
style={{
|
||
"--initial-position-bottom": "200px",
|
||
"--initial-position-right": "24px",
|
||
"--edge-distance": "24px",
|
||
}}
|
||
onClick={handleMainClick}
|
||
>
|
||
<div
|
||
style={{
|
||
width: "100%",
|
||
height: "100%",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 24,
|
||
color: "white",
|
||
background: visible
|
||
? "linear-gradient(135deg, #ff4d4f, #ff7875)"
|
||
: "linear-gradient(135deg, #fff, #fff)",
|
||
borderRadius: "50%",
|
||
transition: "all 0.3s ease",
|
||
transform: visible ? "rotate(45deg)" : "rotate(0deg)",
|
||
boxShadow: visible
|
||
? "0 6px 16px rgba(255, 77, 79, 0.4)"
|
||
: "0 4px 12px #eee",
|
||
}}
|
||
>
|
||
{props.value?.icon}
|
||
<div className="cat">
|
||
{/* {!visible && <CheckOutline style={{ marginRight: "3px" }} />} */}
|
||
切换语言
|
||
</div>
|
||
</div>
|
||
</FloatingBubble>
|
||
</div>
|
||
|
||
{/* 菜单 - 只在有位置信息时渲染 */}
|
||
{visible &&
|
||
menuPosition.x > 0 &&
|
||
menuPosition.y > 0 &&
|
||
createPortal(<MenuComponent />, document.body)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default FloatingFanMenu;
|