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 5c1f2cc..6783693 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,40 @@ 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 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) => @@ -1650,13 +1684,24 @@ export default function NodePage() { node.expiryTime, node.renewalCycle, ); + const connectionStatusMeta = getConnectionStatusMeta( + node.connectionStatus, + ); + const hasRemark = Boolean(node.remark?.trim()); + 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) => (
@@ -1683,51 +1728,103 @@ export default function NodePage() { onValueChange={() => toggleSelect(node.id)} /> )} -

- {node.name} -

+
+
+

+ {node.name} +

+ +
+
-
- {isRemoteNode && ( - + {hasInfoTrigger && ( +
+ +
- 远程 - - )} - {(() => { - const connectionStatusMeta = - getConnectionStatusMeta( - node.connectionStatus, - ); +
+ {hasExpiryInfo && ( +
+
+ 到期提醒 +
+
+ + {expiryMeta.label} + +
+
+ {formatNodeRenewalTime(expiryMeta.nextDueTime)} +
+
+ )} - return ( - - {connectionStatusMeta.text} - - ); - })()} - {node.expiryTime && - node.expiryTime > 0 && - node.renewalCycle && ( - - {expiryMeta.label} - - )} + {hasRemark && ( +
+
+ 备注 +
+
+ {node.remark?.trim()} +
+
+ )} +
+
+
+ )}
@@ -2051,14 +2148,6 @@ export default function NodePage() { )}
- {node.remark?.trim() && ( -
-
- {node.remark.trim()} -
-
- )} - {/* 操作按钮 */}
{!isRemoteNode && (