mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat: 悬浮球-glassmorphism+拖拽+单击开面板+双击重Roll+状态同步
This commit is contained in:
3
index.js
3
index.js
@@ -1076,6 +1076,9 @@ function notifyStatusToast(key, kind, message, title = "ST-BME") {
|
||||
function setRuntimeStatus(text, meta, level = "info") {
|
||||
runtimeStatus = createUiStatus(text, meta, level);
|
||||
refreshPanelLiveState();
|
||||
// 同步悬浮球状态
|
||||
const fabStatus = level === "info" ? "idle" : level;
|
||||
_panelModule?.updateFloatingBallStatus?.(fabStatus, text || "BME 记忆图谱");
|
||||
}
|
||||
|
||||
function setLastExtractionStatus(
|
||||
|
||||
153
panel.js
153
panel.js
@@ -222,6 +222,159 @@ export async function initPanel({
|
||||
_applyWorkspaceMode();
|
||||
_syncConfigSectionState();
|
||||
_refreshRuntimeStatus();
|
||||
_initFloatingBall();
|
||||
}
|
||||
|
||||
// ==================== 悬浮球 ====================
|
||||
|
||||
const FAB_STORAGE_KEY = "bme-fab-position";
|
||||
let _fabEl = null;
|
||||
|
||||
function _initFloatingBall() {
|
||||
if (document.getElementById("bme-floating-ball")) return;
|
||||
|
||||
const fab = document.createElement("div");
|
||||
fab.id = "bme-floating-ball";
|
||||
fab.setAttribute("data-status", "idle");
|
||||
fab.innerHTML = `
|
||||
<i class="fa-solid fa-brain bme-fab-icon"></i>
|
||||
<span class="bme-fab-tooltip">BME 记忆图谱</span>
|
||||
`;
|
||||
document.body.appendChild(fab);
|
||||
_fabEl = fab;
|
||||
|
||||
// 恢复位置
|
||||
const saved = _loadFabPosition();
|
||||
if (saved) {
|
||||
fab.style.left = `${saved.x}px`;
|
||||
fab.style.top = `${saved.y}px`;
|
||||
fab.style.right = "auto";
|
||||
fab.style.bottom = "auto";
|
||||
} else {
|
||||
fab.style.right = "16px";
|
||||
fab.style.bottom = "80px";
|
||||
}
|
||||
|
||||
// 拖拽 + 点击逻辑
|
||||
let isDragging = false;
|
||||
let hasMoved = false;
|
||||
let startX = 0, startY = 0;
|
||||
let fabStartX = 0, fabStartY = 0;
|
||||
let clickTimer = null;
|
||||
|
||||
const DRAG_THRESHOLD = 5;
|
||||
const DBLCLICK_DELAY = 280;
|
||||
|
||||
function onPointerDown(e) {
|
||||
isDragging = true;
|
||||
hasMoved = false;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
const rect = fab.getBoundingClientRect();
|
||||
fabStartX = rect.left;
|
||||
fabStartY = rect.top;
|
||||
fab.setPointerCapture(e.pointerId);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function onPointerMove(e) {
|
||||
if (!isDragging) return;
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
if (!hasMoved && Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return;
|
||||
hasMoved = true;
|
||||
|
||||
let newX = fabStartX + dx;
|
||||
let newY = fabStartY + dy;
|
||||
// 限制在视口内
|
||||
const size = 46;
|
||||
newX = Math.max(0, Math.min(window.innerWidth - size, newX));
|
||||
newY = Math.max(0, Math.min(window.innerHeight - size, newY));
|
||||
|
||||
fab.style.left = `${newX}px`;
|
||||
fab.style.top = `${newY}px`;
|
||||
fab.style.right = "auto";
|
||||
fab.style.bottom = "auto";
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
fab.releasePointerCapture(e.pointerId);
|
||||
|
||||
if (hasMoved) {
|
||||
// 拖拽结束 → 保存位置
|
||||
_saveFabPosition(parseInt(fab.style.left), parseInt(fab.style.top));
|
||||
return;
|
||||
}
|
||||
|
||||
// 非拖拽 → 处理单击/双击
|
||||
if (clickTimer) {
|
||||
// 第二次点击 → 双击 → 重 Roll
|
||||
clearTimeout(clickTimer);
|
||||
clickTimer = null;
|
||||
_onFabDoubleClick();
|
||||
} else {
|
||||
// 第一次点击 → 等待双击
|
||||
clickTimer = setTimeout(() => {
|
||||
clickTimer = null;
|
||||
_onFabSingleClick();
|
||||
}, DBLCLICK_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
fab.addEventListener("pointerdown", onPointerDown);
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
}
|
||||
|
||||
function _onFabSingleClick() {
|
||||
openPanel();
|
||||
}
|
||||
|
||||
async function _onFabDoubleClick() {
|
||||
if (!_actionHandlers.reroll) return;
|
||||
if (!confirm("确认重新提取最新 AI 楼?")) return;
|
||||
|
||||
try {
|
||||
_fabEl?.setAttribute("data-status", "running");
|
||||
await _actionHandlers.reroll({});
|
||||
_fabEl?.setAttribute("data-status", "success");
|
||||
_refreshDashboard();
|
||||
_refreshGraph();
|
||||
setTimeout(() => {
|
||||
const status = _getRuntimeStatus?.() || {};
|
||||
_fabEl?.setAttribute("data-status", status.status || "idle");
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
console.error("[ST-BME] FAB reroll failed:", err);
|
||||
_fabEl?.setAttribute("data-status", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function _loadFabPosition() {
|
||||
try {
|
||||
const raw = localStorage.getItem(FAB_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const pos = JSON.parse(raw);
|
||||
if (Number.isFinite(pos.x) && Number.isFinite(pos.y)) return pos;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _saveFabPosition(x, y) {
|
||||
try {
|
||||
localStorage.setItem(FAB_STORAGE_KEY, JSON.stringify({ x, y }));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function updateFloatingBallStatus(status = "idle", tooltipText = "") {
|
||||
if (!_fabEl) return;
|
||||
_fabEl.setAttribute("data-status", status);
|
||||
if (tooltipText) {
|
||||
const tip = _fabEl.querySelector(".bme-fab-tooltip");
|
||||
if (tip) tip.textContent = tooltipText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
101
style.css
101
style.css
@@ -2243,3 +2243,104 @@
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* --- Floating Ball (悬浮球) --- */
|
||||
#bme-floating-ball {
|
||||
position: fixed;
|
||||
z-index: 9998;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 50%;
|
||||
background: rgba(30, 30, 40, 0.75);
|
||||
backdrop-filter: blur(12px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255,255,255,0.05) inset;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: box-shadow 0.3s ease, transform 0.15s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
#bme-floating-ball:hover {
|
||||
transform: scale(1.08);
|
||||
border-color: var(--bme-accent, #e1415d);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 12px var(--bme-accent, #e1415d40);
|
||||
}
|
||||
|
||||
#bme-floating-ball:active {
|
||||
cursor: grabbing;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
#bme-floating-ball .bme-fab-icon {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
pointer-events: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
#bme-floating-ball:hover .bme-fab-icon {
|
||||
color: var(--bme-accent, #e1415d);
|
||||
}
|
||||
|
||||
/* Status ring */
|
||||
#bme-floating-ball::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#bme-floating-ball[data-status="running"]::after {
|
||||
border-color: var(--bme-accent, #e1415d);
|
||||
animation: bme-fab-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
#bme-floating-ball[data-status="success"]::after {
|
||||
border-color: var(--bme-accent2, #4edea3);
|
||||
}
|
||||
|
||||
#bme-floating-ball[data-status="error"]::after {
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
#bme-floating-ball[data-status="warning"]::after {
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
@keyframes bme-fab-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
#bme-floating-ball .bme-fab-tooltip {
|
||||
position: absolute;
|
||||
right: calc(100% + 10px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(20, 20, 28, 0.92);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 0.72rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
#bme-floating-ball:hover .bme-fab-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user