From 636e1ff1ccb268a37c3f64a65d1a5fd25ff3db2a Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Fri, 27 Mar 2026 12:53:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=82=AC=E6=B5=AE=E7=90=83-glassmorphi?= =?UTF-8?q?sm+=E6=8B=96=E6=8B=BD+=E5=8D=95=E5=87=BB=E5=BC=80=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF+=E5=8F=8C=E5=87=BB=E9=87=8DRoll+=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 3 ++ panel.js | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 101 +++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) diff --git a/index.js b/index.js index 1958943..eefb3cd 100644 --- a/index.js +++ b/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( diff --git a/panel.js b/panel.js index 7c96ed5..41174fe 100644 --- a/panel.js +++ b/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 = ` + + BME 记忆图谱 + `; + 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; + } } /** diff --git a/style.css b/style.css index c84840d..b0d096b 100644 --- a/style.css +++ b/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; +} +