This commit is contained in:
2025-09-03 15:06:16 +08:00
commit 39fff8c63c
96 changed files with 13215 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
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 dogSvg from "@/assets/translate/dog.svg";
import catSvg from "@/assets/translate/cat.svg";
import pigSvg from "@/assets/translate/pig.svg";
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;