refactor: make each floating button independently draggable

This commit is contained in:
YuanShiJiLoong committed 2026-02-22 20:48:09 +09:00
1 parent 27d020e843
commit 58099578c9
1 file changed
+72 -87
+72 -87
View File
@@ -295,78 +295,67 @@ export default function Home() {
</button>
)}
{/* 可拖动浮动按钮组 */}
{(() => {
const STORAGE_KEY = 'author-fab-pos';
const saved = typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null') : null;
const defaultPos = { right: 24, bottom: 24 };
return (
<div
id="tour-fab-group"
style={{
position: 'absolute',
right: `${saved?.right ?? defaultPos.right}px`,
bottom: `${saved?.bottom ?? defaultPos.bottom}px`,
zIndex: 40,
display: 'flex',
flexDirection: 'column',
gap: '8px',
cursor: 'grab',
userSelect: 'none',
}}
onPointerDown={(e) => {
if (e.target.closest('a') || (e.target.tagName === 'BUTTON' && !e.target.classList.contains('fab-drag-handle'))) return;
const el = e.currentTarget;
const rect = el.getBoundingClientRect();
const parentRect = el.parentElement.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
el.style.cursor = 'grabbing';
let moved = false;
const onMove = (ev) => {
moved = true;
const newRight = parentRect.right - ev.clientX - (rect.width - offsetX);
const newBottom = parentRect.bottom - ev.clientY - (rect.height - offsetY);
const clampedRight = Math.max(0, Math.min(parentRect.width - rect.width, newRight));
const clampedBottom = Math.max(0, Math.min(parentRect.height - rect.height, newBottom));
el.style.right = `${clampedRight}px`;
el.style.bottom = `${clampedBottom}px`;
};
const onUp = () => {
el.style.cursor = 'grab';
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
if (moved) {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
right: parseInt(el.style.right),
bottom: parseInt(el.style.bottom),
}));
}
};
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
}}
>
{/* 独立可拖动浮动按钮 */}
{['github', 'help'].map(btnKey => {
const storageKey = `author-fab-${btnKey}`;
const saved = typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(storageKey) || 'null') : null;
const defaults = { github: { right: 24, bottom: 76 }, help: { right: 24, bottom: 24 } };
const pos = saved || defaults[btnKey];
const makeDraggable = (e) => {
const el = e.currentTarget;
const parentRect = el.parentElement.getBoundingClientRect();
const rect = el.getBoundingClientRect();
const startX = e.clientX, startY = e.clientY;
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
let dragging = false;
const onMove = (ev) => {
if (!dragging && Math.abs(ev.clientX - startX) + Math.abs(ev.clientY - startY) < 5) return;
dragging = true;
ev.preventDefault();
const r = Math.max(0, Math.min(parentRect.width - rect.width, parentRect.right - ev.clientX - (rect.width - offsetX)));
const b = Math.max(0, Math.min(parentRect.height - rect.height, parentRect.bottom - ev.clientY - (rect.height - offsetY)));
el.style.right = `${r}px`;
el.style.bottom = `${b}px`;
};
const onUp = (ev) => {
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
if (dragging) {
ev.preventDefault();
localStorage.setItem(storageKey, JSON.stringify({ right: parseInt(el.style.right), bottom: parseInt(el.style.bottom) }));
}
};
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
};
const commonStyle = {
position: 'absolute',
right: `${pos.right}px`,
bottom: `${pos.bottom}px`,
zIndex: 40,
borderRadius: '50%',
width: '44px',
height: '44px',
boxShadow: 'var(--shadow-md)',
fontSize: '18px',
opacity: 0.8,
cursor: 'grab',
userSelect: 'none',
touchAction: 'none',
};
if (btnKey === 'github') {
return (
<a
key="github"
id="tour-github"
href="https://github.com/YuanShiJiLoong/author"
target="_blank"
rel="noopener noreferrer"
className="btn btn-secondary btn-icon"
style={{
borderRadius: '50%',
width: '44px',
height: '44px',
boxShadow: 'var(--shadow-md)',
fontSize: '18px',
transition: 'opacity 0.15s',
opacity: 0.8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textDecoration: 'none',
color: 'inherit',
}}
style={{ ...commonStyle, display: 'flex', alignItems: 'center', justifyContent: 'center', textDecoration: 'none', color: 'inherit' }}
onPointerDown={makeDraggable}
onClick={(e) => { if (e.defaultPrevented) return; }}
onMouseEnter={(e) => e.currentTarget.style.opacity = '1'}
onMouseLeave={(e) => e.currentTarget.style.opacity = '0.8'}
title="GitHub"
@@ -375,28 +364,24 @@ export default function Home() {
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
</a>
<button
id="tour-help"
className="btn btn-secondary btn-icon"
style={{
borderRadius: '50%',
width: '44px',
height: '44px',
boxShadow: 'var(--shadow-md)',
fontSize: '18px',
transition: 'opacity 0.15s',
opacity: 0.8
}}
onMouseEnter={(e) => e.currentTarget.style.opacity = '1'}
onMouseLeave={(e) => e.currentTarget.style.opacity = '0.8'}
onClick={() => setShowHelp(true)}
title={t('page.helpAndGuide')}
>
📖
</button>
</div>
);
}
return (
<button
key="help"
id="tour-help"
className="btn btn-secondary btn-icon"
style={commonStyle}
onPointerDown={makeDraggable}
onClick={(e) => { if (!e.defaultPrevented) setShowHelp(true); }}
onMouseEnter={(e) => e.currentTarget.style.opacity = '1'}
onMouseLeave={(e) => e.currentTarget.style.opacity = '0.8'}
title={t('page.helpAndGuide')}
>
📖
</button>
);
})()}
})}
</main>
{/* ===== AI 对话侧栏 ===== */}