受 Echors 启发,我做了一个js脚本,可以根据自己需求定制一个悬浮命令按钮。你需要修改command id 以及 lucide图标名
如下图所示,左键为默认按钮,右键为展开命令按钮,按钮可随意拖动(移动端:短按为默认按钮,长按不动为展开命令按钮,长按拖动可随意拖动)
最后的效果如下:

V2版本增加了边缘吸附功能(同样支持移动端)
脚本:
V1
// == Obsidian 可拖动自定义按钮脚本(增强版)==
(function() {
'use strict';
console.log('🚀 初始化自定义浮动按钮脚本(增强版)');
// 配置项 - 可修改
// 左键配置
const CONFIG = {
commandId: 'form-flow:@CFORM_00配置/components/form/标签切换.cform',
commandName: '标签面板',
defaultPosition: { right: 50, bottom: 50 },
// 右键配置
statusBarCommands: [
{id: 'command-palette:open', icon: 'command', name: '命令'},
{id: 'app:open-settings', icon: 'settings', name: '设置'},
{id: 'remotely-save:start-sync', icon: 'folder-sync', name: '同步'}
],
menuAnimationDuration: 200,
longPressDuration: 500,
longPressMoveThreshold: 15 // 长按移动阈值(像素)
};
let button = null;
let menu = null;
let isDragging = false;
let startX = 0;
let startY = 0;
let startRight = 0;
let startBottom = 0;
let dragDistance = 0;
const dragThreshold = 5;
let longPressTimer = null;
let touchStartPoint = null;
let menuTriggered = false; // 新增:标记菜单是否已触发
// 创建按钮元素
function createButton() {
// 如果按钮已存在,则先移除
const existingButton = document.getElementById('obsidian-custom-floating-button');
if (existingButton) existingButton.remove();
// 创建按钮元素
button = document.createElement('div');
button.id = 'obsidian-custom-floating-button';
button.className = 'custom-floating-button';
// 添加SVG图标
const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgIcon.setAttribute('width', '24');
svgIcon.setAttribute('height', '24');
svgIcon.setAttribute('viewBox', '0 0 24 24');
svgIcon.setAttribute('fill', 'none');
svgIcon.setAttribute('stroke', 'currentColor');
svgIcon.setAttribute('stroke-width', '2');
svgIcon.setAttribute('stroke-linecap', 'round');
svgIcon.setAttribute('stroke-linejoin', 'round');
const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect1.setAttribute('x', '3');
rect1.setAttribute('y', '3');
rect1.setAttribute('width', '18');
rect1.setAttribute('height', '18');
rect1.setAttribute('rx', '2');
rect1.setAttribute('ry', '2');
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'M9 3v18');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M15 3v18');
svgIcon.appendChild(rect1);
svgIcon.appendChild(path1);
svgIcon.appendChild(path2);
button.appendChild(svgIcon);
// 添加提示文本
const tooltip = document.createElement('div');
tooltip.className = 'command-tooltip';
tooltip.textContent = `${CONFIG.commandName}`;
button.appendChild(tooltip);
// 设置初始样式
button.style.position = 'fixed';
button.style.zIndex = '9999';
button.style.width = '50px';
button.style.height = '50px';
button.style.borderRadius = '50%';
button.style.backgroundColor = 'var(--interactive-accent)';
button.style.color = 'white';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.cursor = 'move';
button.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)';
button.style.transition = 'transform 0.2s, box-shadow 0.2s, background-color 0.2s';
button.style.userSelect = 'none';
button.style.fontSize = '24px';
button.style.fontWeight = 'bold';
button.style.touchAction = 'none';
// 图标样式
svgIcon.style.width = '24px';
svgIcon.style.height = '24px';
svgIcon.style.pointerEvents = 'none';
// 工具提示样式
tooltip.style.position = 'absolute';
tooltip.style.top = '-30px';
tooltip.style.background = 'var(--background-secondary)';
tooltip.style.color = 'var(--text-normal)';
tooltip.style.padding = '4px 8px';
tooltip.style.borderRadius = '4px';
tooltip.style.fontSize = '12px';
tooltip.style.whiteSpace = 'nowrap';
tooltip.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.3)';
tooltip.style.opacity = '0';
tooltip.style.transition = 'opacity 0.3s';
tooltip.style.pointerEvents = 'none';
tooltip.style.transform = 'translateX(-50%)';
tooltip.style.left = '50%';
// 添加到文档
document.body.appendChild(button);
// 创建命令菜单
createCommandMenu();
// 绑定事件
bindEvents();
}
// 创建命令菜单
function createCommandMenu() {
// 移除已存在的菜单
const existingMenu = document.getElementById('obsidian-command-menu');
if (existingMenu) existingMenu.remove();
menu = document.createElement('div');
menu.id = 'obsidian-command-menu';
menu.className = 'command-menu';
menu.style.display = 'none';
menu.style.position = 'fixed';
menu.style.zIndex = '10000';
menu.style.backgroundColor = 'var(--background-primary)';
menu.style.borderRadius = '8px';
menu.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.3)';
menu.style.padding = '8px';
menu.style.transition = `opacity ${CONFIG.menuAnimationDuration}ms, transform ${CONFIG.menuAnimationDuration}ms`;
menu.style.opacity = '0';
menu.style.transform = 'scale(0.8)';
menu.style.transformOrigin = 'bottom center';
menu.style.userSelect = 'none';
menu.style.pointerEvents = 'none';
// 创建菜单项
CONFIG.statusBarCommands.forEach(command => {
const menuItem = document.createElement('div');
menuItem.className = 'menu-item';
menuItem.dataset.commandId = command.id;
// 添加图标
const iconSvg = createIconSvg(command.icon);
menuItem.appendChild(iconSvg);
// 添加标签
const label = document.createElement('span');
label.className = 'menu-label';
label.textContent = command.name;
menuItem.appendChild(label);
// 样式设置
menuItem.style.display = 'flex';
menuItem.style.alignItems = 'center';
menuItem.style.padding = '8px 12px';
menuItem.style.borderRadius = '4px';
menuItem.style.cursor = 'pointer';
menuItem.style.marginBottom = '4px';
menuItem.style.transition = 'background-color 0.2s';
// 悬停效果
menuItem.addEventListener('mouseenter', () => {
menuItem.style.backgroundColor = 'var(--background-secondary)';
});
menuItem.addEventListener('mouseleave', () => {
menuItem.style.backgroundColor = '';
});
// 点击执行命令
menuItem.addEventListener('click', (e) => {
e.stopPropagation();
executeCommandById(command.id);
hideMenu();
});
menu.appendChild(menuItem);
});
document.body.appendChild(menu);
}
// 创建图标SVG
function createIconSvg(iconName) {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
const path = document.createElementNS(svgNS, 'path');
switch(iconName) {
case 'folder-sync':
path.setAttribute('d', 'M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12s4.48 10 10 10');
break;
case 'settings':
path.setAttribute('d', 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z');
break;
case 'command':
path.setAttribute('d', 'M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z');
break;
default:
path.setAttribute('d', 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z');
}
svg.appendChild(path);
return svg;
}
// 显示命令菜单
function showMenu() {
if (!menu || !button) return;
// 计算菜单位置
const buttonRect = button.getBoundingClientRect();
const menuWidth = 180;
const menuItemHeight = 36;
const menuHeight = CONFIG.statusBarCommands.length * menuItemHeight + 16;
// 计算上方空间是否足够
const spaceAbove = buttonRect.top;
const spaceBelow = window.innerHeight - buttonRect.bottom;
let menuTop, menuLeft;
// 优先向上展开
if (spaceAbove >= menuHeight || spaceAbove >= spaceBelow) {
menuTop = buttonRect.top - menuHeight - 10;
menu.style.transformOrigin = 'bottom center';
}
// 向上空间不足时向下展开
else {
menuTop = buttonRect.bottom + 10;
menu.style.transformOrigin = 'top center';
}
// 水平居中
menuLeft = buttonRect.left + (buttonRect.width - menuWidth) / 2;
// 边界检查
if (menuLeft < 10) menuLeft = 10;
if (menuLeft + menuWidth > window.innerWidth - 10) {
menuLeft = window.innerWidth - menuWidth - 10;
}
// 应用位置
menu.style.left = `${menuLeft}px`;
menu.style.top = `${menuTop}px`;
menu.style.display = 'block';
menu.style.pointerEvents = 'auto';
// 触发动画
setTimeout(() => {
menu.style.opacity = '1';
menu.style.transform = 'scale(1)';
}, 10);
// 添加全局点击关闭监听
setTimeout(() => {
document.addEventListener('click', globalClickHandler);
}, 50);
}
// 隐藏命令菜单
function hideMenu() {
if (!menu) return;
menu.style.opacity = '0';
menu.style.transform = 'scale(0.8)';
// 动画结束后隐藏
setTimeout(() => {
menu.style.display = 'none';
menu.style.pointerEvents = 'none';
document.removeEventListener('click', globalClickHandler);
}, CONFIG.menuAnimationDuration);
}
// 全局点击处理(关闭菜单)
function globalClickHandler(e) {
if (menu && !menu.contains(e.target) && e.target !== button) {
hideMenu();
}
}
// 绑定事件处理
function bindEvents() {
// 桌面端:拖动开始
button.addEventListener('mousedown', function(e) {
if (e.button === 0) {
if (e.target === button || e.target.className === 'command-tooltip') {
e.preventDefault();
startDrag(e.clientX, e.clientY);
}
}
});
// 桌面端:右键菜单
button.addEventListener('contextmenu', function(e) {
e.preventDefault();
showMenu();
});
// 移动端:触摸开始
button.addEventListener('touchstart', function(e) {
if (e.target === button || e.target.className === 'command-tooltip') {
e.preventDefault();
const touch = e.touches[0];
// 重置菜单触发标记
menuTriggered = false;
// 记录起始点位置
touchStartPoint = {
x: touch.clientX,
y: touch.clientY
};
// 设置长按计时器
longPressTimer = setTimeout(() => {
// 检查是否仍在按钮上
if (longPressTimer) {
showMenu();
menuTriggered = true; // 标记菜单已触发
// 取消拖动状态
isDragging = false;
button.style.cursor = 'move';
button.style.opacity = '';
button.style.transition = '';
longPressTimer = null;
}
}, CONFIG.longPressDuration);
startDrag(touch.clientX, touch.clientY);
}
}, { passive: false });
// 移动端:触摸移动
button.addEventListener('touchmove', function(e) {
if (!touchStartPoint) return;
const touch = e.touches[0];
const dx = Math.abs(touch.clientX - touchStartPoint.x);
const dy = Math.abs(touch.clientY - touchStartPoint.y);
const distance = Math.sqrt(dx * dx + dy * dy);
// 如果移动距离超过阈值,取消长按菜单
if (distance > CONFIG.longPressMoveThreshold && longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
// 移动端:触摸结束
button.addEventListener('touchend', function(e) {
// 清除长按计时器
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
// 如果菜单已触发,重置标记并返回
if (menuTriggered) {
menuTriggered = false;
return;
}
touchStartPoint = null;
// 执行默认命令(仅在未触发菜单且拖动距离小于阈值时)
if (dragDistance < dragThreshold) {
console.log(`📱 执行命令: ${CONFIG.commandId}`);
executeCommand();
}
});
// 点击执行命令(桌面端)
button.addEventListener('click', function(e) {
if (dragDistance < dragThreshold && e.button === 0) {
console.log(`🔍 执行命令: ${CONFIG.commandId}`);
executeCommand();
}
});
// 鼠标悬停效果(仅桌面端)
button.addEventListener('mouseenter', function() {
const tooltip = button.querySelector('.command-tooltip');
tooltip.style.opacity = '1';
button.style.backgroundColor = 'var(--interactive-accent-hover)';
});
button.addEventListener('mouseleave', function() {
const tooltip = button.querySelector('.command-tooltip');
tooltip.style.opacity = '0';
button.style.backgroundColor = 'var(--interactive-accent)';
});
}
// 开始拖动
function startDrag(clientX, clientY) {
isDragging = true;
dragDistance = 0;
startX = clientX;
startY = clientY;
// 获取当前位置
startRight = parseInt(button.style.right) || CONFIG.defaultPosition.right;
startBottom = parseInt(button.style.bottom) || CONFIG.defaultPosition.bottom;
// 缓存初始位置和尺寸
button._dragCache = {
width: button.offsetWidth,
height: button.offsetHeight
};
button.style.cursor = 'grabbing';
button.style.opacity = '0.9';
button.style.transition = 'none';
console.log('🎯 开始拖动按钮');
}
// 执行Obsidian命令
function executeCommand() {
executeCommandById(CONFIG.commandId);
}
// 通过ID执行命令
function executeCommandById(commandId) {
// 等待app对象可用
const waitForApp = setInterval(() => {
if (window.app) {
clearInterval(waitForApp);
try {
// 执行命令
window.app.commands.executeCommandById(commandId);
console.log(`✅ 成功执行命令: ${commandId}`);
// 添加视觉反馈
button.style.transform = 'scale(0.95)';
button.style.backgroundColor = 'var(--interactive-accent-active)';
setTimeout(() => {
button.style.transform = '';
button.style.backgroundColor = 'var(--interactive-accent)';
}, 200);
} catch (error) {
console.error(`❌ 执行命令失败: ${error}`);
button.style.backgroundColor = '#ff6b6b';
setTimeout(() => {
button.style.backgroundColor = 'var(--interactive-accent)';
}, 1000);
}
}
}, 100);
}
// 加载保存的位置
function loadPosition() {
const savedPosition = localStorage.getItem('obsidianFloatingButtonPosition');
if (savedPosition) {
try {
const position = JSON.parse(savedPosition);
// 直接应用新位置
if (position.right !== undefined && position.bottom !== undefined) {
button.style.right = `${position.right}px`;
button.style.bottom = `${position.bottom}px`;
console.log('📌 加载保存的按钮位置');
return;
}
} catch (e) {
console.error('❌ 位置数据解析失败', e);
}
}
setDefaultPosition();
}
// 设置默认位置
function setDefaultPosition() {
button.style.right = `${CONFIG.defaultPosition.right}px`;
button.style.bottom = `${CONFIG.defaultPosition.bottom}px`;
console.log('⚙️ 使用默认按钮位置');
}
// 保存位置到本地存储
function savePosition() {
const position = {
right: parseInt(button.style.right) || CONFIG.defaultPosition.right,
bottom: parseInt(button.style.bottom) || CONFIG.defaultPosition.bottom
};
localStorage.setItem('obsidianFloatingButtonPosition', JSON.stringify(position));
console.log('💾 保存按钮位置');
}
// 处理拖动移动
function handleDragMove(clientX, clientY) {
if (!isDragging || !button) return;
// 计算移动距离
const deltaX = startX - clientX;
const deltaY = startY - clientY;
// 更新拖动距离
dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
// 更新位置
const newRight = startRight + deltaX;
const newBottom = startBottom + deltaY;
// 边界检查
const minRight = 10;
const minBottom = 10;
const maxRight = window.innerWidth - button._dragCache.width - 10;
const maxBottom = window.innerHeight - button._dragCache.height - 10;
button.style.right = `${Math.max(minRight, Math.min(newRight, maxRight))}px`;
button.style.bottom = `${Math.max(minBottom, Math.min(newBottom, maxBottom))}px`;
}
// 使用requestAnimationFrame优化桌面端拖动
function setupDesktopDrag() {
let lastMoveTime = 0;
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
// 使用节流优化性能
const now = Date.now();
if (now - lastMoveTime < 16) return;
lastMoveTime = now;
handleDragMove(e.clientX, e.clientY);
});
}
// 移动端拖动处理
function setupMobileDrag() {
document.addEventListener('touchmove', function(e) {
if (!isDragging || !button) return;
const touch = e.touches[0];
handleDragMove(touch.clientX, touch.clientY);
// 防止页面滚动
if (dragDistance > 10) {
e.preventDefault();
}
}, { passive: false });
}
// 结束拖动
function endDrag() {
if (isDragging) {
isDragging = false;
button.style.cursor = 'move';
button.style.opacity = '';
button.style.transition = 'transform 0.2s, box-shadow 0.2s, background-color 0.2s';
delete button._dragCache;
savePosition();
console.log('✅ 拖动结束');
}
}
// 窗口大小变化处理
function handleWindowResize() {
if (!button) return;
// 获取当前位置
const currentRight = parseInt(button.style.right) || CONFIG.defaultPosition.right;
const currentBottom = parseInt(button.style.bottom) || CONFIG.defaultPosition.bottom;
// 检查按钮是否在可视区域外
const maxRight = window.innerWidth - button.offsetWidth - 10;
const maxBottom = window.innerHeight - button.offsetHeight - 10;
if (currentRight > maxRight || currentBottom > maxBottom) {
// 自动调整到安全位置
button.style.right = `${Math.min(currentRight, maxRight)}px`;
button.style.bottom = `${Math.min(currentBottom, maxBottom)}px`;
savePosition();
console.log('🔄 窗口大小变化,自动调整按钮位置');
}
// 如果菜单显示,重新定位
if (menu && menu.style.display === 'block') {
showMenu();
}
}
// 添加重置按钮位置的功能
window.resetFloatingButton = function() {
localStorage.removeItem('obsidianFloatingButtonPosition');
setDefaultPosition();
console.log('🔄 按钮位置已重置');
};
// 初始化
function init() {
createButton();
loadPosition();
// 设置事件监听
setupDesktopDrag();
setupMobileDrag();
// 添加窗口大小变化监听
let resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleWindowResize, 200);
});
// 桌面端释放
document.addEventListener('mouseup', endDrag);
// 移动端释放
document.addEventListener('touchend', endDrag);
console.log('✅ 自定义浮动按钮已创建');
}
// 在DOM加载完成后初始化
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(init, 100);
} else {
document.addEventListener('DOMContentLoaded', init);
}
console.log('🎉 自定义浮动按钮脚本加载完成(增强版)');
})();
V2
食用方法:
目前找到两种方式触发脚本
方式一:安装插件 obsidian-form-flow,直接把js脚本放在form-flow的脚本文件夹下即可加载脚本
方式二:安装插件 obsidian-js-engine-plugin ,加载此脚本