From ff9440694528b649b777d974ef326d071dabc95a Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:42:23 +0800 Subject: [PATCH] feat(node): restore info button popover style for remark/renewal info (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the 2.1.8-beta9 style of displaying node remark and renewal info via an info button (ℹ️) in the CardHeader with a hover popover, instead of inline display in the CardBody. - Add infoPopoverPlacement state and updateInfoPopoverPlacement callback - Add info button with hover popover showing expiry reminder and remark - Restore drag handle with touch support and responsive visibility - Remove inline info display from CardBody --- vite-frontend/src/pages/node.tsx | 224 +++++++++++++++++++++---------- 1 file changed, 152 insertions(+), 72 deletions(-) diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index b98733d..2613dc1 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -375,6 +375,41 @@ export default function NodePage() { Record >({}); + const [infoPopoverPlacement, setInfoPopoverPlacement] = useState< + Record + >({}); + + const updateInfoPopoverPlacement = useCallback( + (nodeId: number, triggerElement: HTMLElement | null) => { + if (!triggerElement) { + return; + } + + const rect = triggerElement.getBoundingClientRect(); + const cardElement = triggerElement.closest("[data-node-card='true']"); + const cardRect = + cardElement instanceof HTMLElement + ? cardElement.getBoundingClientRect() + : null; + const estimatedPanelWidth = 288; + const containerPadding = 16; + const availableLeftSpace = cardRect + ? rect.left - cardRect.left + : rect.left; + const nextPlacement: "left" | "bottom" = + availableLeftSpace >= estimatedPanelWidth + containerPadding + ? "left" + : "bottom"; + + setInfoPopoverPlacement((prev) => + prev[nodeId] === nextPlacement + ? prev + : { ...prev, [nodeId]: nextPlacement }, + ); + }, + [], + ); + const handleNodeOffline = useCallback((nodeId: number) => { setNodeList((prev) => prev.map((node) => { @@ -1710,10 +1745,12 @@ export default function NodePage() { !node.expiryReminderDismissed, ); const hasInfoTrigger = hasRemark || hasExpiryInfo; + const infoCount = Number(hasExpiryInfo) + Number(hasRemark); + const infoPlacement = infoPopoverPlacement[node.id] ?? "left"; return ( - {(listeners, attributes) => ( + {(listeners) => (
+
+ +
{selectMode && (
-
-
- -
+
+ {hasInfoTrigger && ( +
+ +
+
+ {hasExpiryInfo && ( +
+
+
+ 到期提醒 +
+ +
+
+ + {expiryMeta.label} + +
+
+ {formatNodeRenewalTime( + expiryMeta.nextDueTime, + )} +
+
+ )} + + {hasRemark && ( +
+
+ 备注 +
+
+ {node.remark?.trim()} +
+
+ )} +
+
+
+ )}
@@ -2114,60 +2248,6 @@ export default function NodePage() { )} - {hasInfoTrigger && ( -
- {hasExpiryInfo && ( -
-
-
- 到期提醒 -
- -
-
- - {expiryMeta.label} - -
-
- {formatNodeRenewalTime( - expiryMeta.nextDueTime, - )} -
-
- )} - - {hasRemark && ( -
-
- 备注 -
-
- {node.remark?.trim()} -
-
- )} -
- )} -
{/* 操作按钮 */}