From 875238321ec74ae57d7a951177f543c074b902f0 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 26 Mar 2026 13:43:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=AF=A9=E6=A0=B8=20Modal=20?= =?UTF-8?q?=E7=9C=9F=E6=AD=A3=E5=85=A8=E5=B1=8F=20-=20Portal=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=88=B0=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修復: 1. 使用 createPortal 渲染到 document.body 2. z-index 提升到 9999 確保最上層 3. ApprovalCard 新增 fullHeight prop 移除高度限制 4. 移除巢狀 div 避免滾動衝突 Co-Authored-By: Claude Opus 4.5 --- .../components/ai/openclaw-state-machine.tsx | 35 +++++++++---------- .../src/components/approval/approval-card.tsx | 7 ++-- apps/web/src/components/ui/approval-modal.tsx | 23 ++++++++---- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/apps/web/src/components/ai/openclaw-state-machine.tsx b/apps/web/src/components/ai/openclaw-state-machine.tsx index 3e2bec026..48b75eeb2 100644 --- a/apps/web/src/components/ai/openclaw-state-machine.tsx +++ b/apps/web/src/components/ai/openclaw-state-machine.tsx @@ -421,25 +421,24 @@ export function OpenClawStateMachine({ total={pendingApprovals.length} > {selectedApproval && ( -
- { - handleApprove(selectedApproval.id) - // 簽核後移到下一個或關閉 - if (selectedIndex !== null && selectedIndex < pendingApprovals.length - 1) { - setSelectedIndex(selectedIndex + 1) - } else { - setSelectedIndex(null) - } - }} - onReject={() => { - handleReject(selectedApproval.id) + { + handleApprove(selectedApproval.id) + // 簽核後移到下一個或關閉 + if (selectedIndex !== null && selectedIndex < pendingApprovals.length - 1) { + setSelectedIndex(selectedIndex + 1) + } else { setSelectedIndex(null) - }} - holdDuration={1000} - /> -
+ } + }} + onReject={() => { + handleReject(selectedApproval.id) + setSelectedIndex(null) + }} + holdDuration={1000} + fullHeight + /> )} diff --git a/apps/web/src/components/approval/approval-card.tsx b/apps/web/src/components/approval/approval-card.tsx index 946ded02d..ad6682072 100644 --- a/apps/web/src/components/approval/approval-card.tsx +++ b/apps/web/src/components/approval/approval-card.tsx @@ -82,6 +82,8 @@ export interface ApprovalCardProps { readOnly?: boolean /** 最終狀態標籤 (歷史紀錄用) */ finalStatus?: 'approved' | 'rejected' | 'executed' | 'failed' + /** 在 Modal 中使用時,移除高度限制 */ + fullHeight?: boolean } // ============================================================================= @@ -263,6 +265,7 @@ export function ApprovalCard({ isLoading = false, readOnly = false, finalStatus, + fullHeight = false, }: ApprovalCardProps) { const t = useTranslations('approval') const tRisk = useTranslations('risk') @@ -331,8 +334,8 @@ export function ApprovalCard({ padding="none" className={cn( 'relative transition-all duration-300 flex flex-col', - // UX 優化: 限制最大高度,內容可滾動 - 'max-h-[70vh]', + // UX 優化: 限制最大高度,內容可滾動 (Modal 模式除外) + !fullHeight && 'max-h-[70vh]', // Nothing.tech 精密極簡風格 !readOnly && 'hover:-translate-y-0.5 hover:shadow-card-hover', // CRITICAL: 左側紅色粗邊框 (乾淨、無光害) diff --git a/apps/web/src/components/ui/approval-modal.tsx b/apps/web/src/components/ui/approval-modal.tsx index 2987c3cfe..75548c1c5 100644 --- a/apps/web/src/components/ui/approval-modal.tsx +++ b/apps/web/src/components/ui/approval-modal.tsx @@ -10,11 +10,13 @@ * - 全屏遮罩強制用戶注意,防止誤操作 * - 居中對話框提供足夠空間顯示所有資訊 * - 符合 Nothing.tech「一次專注一件事」設計哲學 + * - 使用 Portal 渲染到 body,確保不受父容器限制 * * 參考: GitHub PR Review, AWS IAM Console */ -import { useEffect, useCallback } from 'react' +import { useEffect, useCallback, useState } from 'react' +import { createPortal } from 'react-dom' import { cn } from '@/lib/utils' import { X, ChevronLeft, ChevronRight, Shield } from 'lucide-react' @@ -48,6 +50,13 @@ export function ApprovalModal({ if (e.key === 'ArrowRight' && onNext) onNext() }, [onClose, onPrev, onNext]) + // Portal 目標 + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + // 鎖定背景滾動 useEffect(() => { if (open) { @@ -60,10 +69,11 @@ export function ApprovalModal({ } }, [open, handleKeyDown]) - if (!open) return null + if (!open || !mounted) return null - return ( -
+ // 使用 Portal 渲染到 body,確保全屏覆蓋 + return createPortal( +
{/* Backdrop - 深色遮罩,強調聚焦 */}
{/* Content - 可滾動 */} -
+
{children}
-
+
, + document.body ) }