From 5e7bf3ba5cfbe41474a84e1b09291766096f4988 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Tue, 10 Mar 2026 09:48:11 +0800 Subject: [PATCH 1/3] fix(node): compact card metadata for mobile Entire-Checkpoint: b3c79b0fd2f9 --- vite-frontend/src/pages/node.tsx | 153 ++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 52 deletions(-) diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index 5c1f2cc..187373e 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -1650,13 +1650,24 @@ export default function NodePage() { node.expiryTime, node.renewalCycle, ); + const connectionStatusMeta = getConnectionStatusMeta( + node.connectionStatus, + ); + const hasRemark = Boolean(node.remark?.trim()); + const metaCount = + 1 + + (isRemoteNode ? 1 : 0) + + (node.expiryTime && node.expiryTime > 0 && node.renewalCycle + ? 1 + : 0) + + (hasRemark ? 1 : 0); return ( {(listeners) => (
@@ -1683,51 +1694,97 @@ export default function NodePage() { onValueChange={() => toggleSelect(node.id)} /> )} -

- {node.name} -

+
+
+

+ {node.name} +

+ +
+
-
- {isRemoteNode && ( - - 远程 - - )} - {(() => { - const connectionStatusMeta = - getConnectionStatusMeta( - node.connectionStatus, - ); +
+
+ + 信息 + + {metaCount} + + + +
+
+
+
+ 标签 +
+
+ {isRemoteNode && ( + + 远程 + + )} + + {connectionStatusMeta.text} + + {node.expiryTime && + node.expiryTime > 0 && + node.renewalCycle && ( + + {expiryMeta.label} + + )} +
+
- return ( - - {connectionStatusMeta.text} - - ); - })()} - {node.expiryTime && - node.expiryTime > 0 && - node.renewalCycle && ( - - {expiryMeta.label} - - )} + {hasRemark && ( +
+
+ 备注 +
+
+ {node.remark?.trim()} +
+
+ )} +
+
+
@@ -2051,14 +2108,6 @@ export default function NodePage() { )}
- {node.remark?.trim() && ( -
-
- {node.remark.trim()} -
-
- )} - {/* 操作按钮 */}
{!isRemoteNode && ( From 6427b830ea9c9092ba281574c4dd8cf6a37e6e95 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Tue, 10 Mar 2026 17:19:21 +0800 Subject: [PATCH 2/3] refactor(node): improve card metadata popup with hover trigger Entire-Checkpoint: 7a033056acc0 --- vite-frontend/.dockerignore | 18 +++++ vite-frontend/src/pages/node.tsx | 133 +++++++++++++++++++------------ 2 files changed, 99 insertions(+), 52 deletions(-) create mode 100644 vite-frontend/.dockerignore diff --git a/vite-frontend/.dockerignore b/vite-frontend/.dockerignore new file mode 100644 index 0000000..bcfac75 --- /dev/null +++ b/vite-frontend/.dockerignore @@ -0,0 +1,18 @@ +node_modules +dist + +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +.DS_Store +.vscode +.idea + +.env.local +.env.*.local + +coverage +*.tsbuildinfo diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index 187373e..e0e56cc 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -278,7 +278,7 @@ const SortableItem = ({ ref={setNodeRef} style={style} {...attributes} - className="overflow-hidden h-full" + className="overflow-visible h-full" {...listeners} > {children(listeners)} @@ -377,6 +377,30 @@ export default function NodePage() { const [upgradeProgress, setUpgradeProgress] = useState< Record >({}); + const [infoPopoverPlacement, setInfoPopoverPlacement] = useState< + Record + >({}); + + const updateInfoPopoverPlacement = useCallback( + (nodeId: number, triggerElement: HTMLElement | null) => { + if (!triggerElement) { + return; + } + + const rect = triggerElement.getBoundingClientRect(); + const estimatedPanelWidth = 288; + const viewportPadding = 24; + const nextPlacement: "left" | "bottom" = + rect.left >= estimatedPanelWidth + viewportPadding ? "left" : "bottom"; + + setInfoPopoverPlacement((prev) => + prev[nodeId] === nextPlacement + ? prev + : { ...prev, [nodeId]: nextPlacement }, + ); + }, + [], + ); const handleNodeOffline = useCallback((nodeId: number) => { setNodeList((prev) => @@ -1654,20 +1678,19 @@ export default function NodePage() { node.connectionStatus, ); const hasRemark = Boolean(node.remark?.trim()); - const metaCount = - 1 + - (isRemoteNode ? 1 : 0) + - (node.expiryTime && node.expiryTime > 0 && node.renewalCycle - ? 1 - : 0) + - (hasRemark ? 1 : 0); + const hasExpiryInfo = Boolean( + node.expiryTime && node.expiryTime > 0 && node.renewalCycle, + ); + const hasInfoTrigger = hasRemark || hasExpiryInfo; + const infoCount = Number(hasExpiryInfo) + Number(hasRemark); + const infoPlacement = infoPopoverPlacement[node.id] ?? "left"; return ( {(listeners) => (
@@ -1707,67 +1730,72 @@ export default function NodePage() {
-
- - 信息 - - {metaCount} - + {hasInfoTrigger && ( +
+
-
+ {hasRemark && ( + + )} + +
-
-
- 标签 -
-
- {isRemoteNode && ( + {hasExpiryInfo && ( +
+
+ 到期提醒 +
+
- 远程 + {expiryMeta.label} - )} - - {connectionStatusMeta.text} - - {node.expiryTime && - node.expiryTime > 0 && - node.renewalCycle && ( - - {expiryMeta.label} - - )} +
+
+ {formatNodeRenewalTime(expiryMeta.nextDueTime)} +
-
+ )} {hasRemark && (
@@ -1784,7 +1812,8 @@ export default function NodePage() { )}
-
+
+ )}
From f41064086248a5b2ceee22152bf1f8b3930c4cc7 Mon Sep 17 00:00:00 2001 From: sagitchu Date: Tue, 10 Mar 2026 20:11:49 +0800 Subject: [PATCH 3/3] fix: calculate info popover placement relative to card container Changed info popover position calculation to use the card container as reference instead of viewport, ensuring consistent placement within card boundaries. --- vite-frontend/src/pages/node.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index e0e56cc..6783693 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -388,10 +388,20 @@ export default function NodePage() { } const rect = triggerElement.getBoundingClientRect(); + const cardElement = triggerElement.closest("[data-node-card='true']"); + const cardRect = + cardElement instanceof HTMLElement + ? cardElement.getBoundingClientRect() + : null; const estimatedPanelWidth = 288; - const viewportPadding = 24; + const containerPadding = 16; + const availableLeftSpace = cardRect + ? rect.left - cardRect.left + : rect.left; const nextPlacement: "left" | "bottom" = - rect.left >= estimatedPanelWidth + viewportPadding ? "left" : "bottom"; + availableLeftSpace >= estimatedPanelWidth + containerPadding + ? "left" + : "bottom"; setInfoPopoverPlacement((prev) => prev[nodeId] === nextPlacement @@ -1689,6 +1699,7 @@ export default function NodePage() { {(listeners) => (