fix(web): 審核 Modal 真正全屏 - Portal 渲染到 body

修復:
1. 使用 createPortal 渲染到 document.body
2. z-index 提升到 9999 確保最上層
3. ApprovalCard 新增 fullHeight prop 移除高度限制
4. 移除巢狀 div 避免滾動衝突

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 13:43:11 +08:00
parent 540602fd63
commit 875238321e
3 changed files with 39 additions and 26 deletions

View File

@@ -421,25 +421,24 @@ export function OpenClawStateMachine({
total={pendingApprovals.length}
>
{selectedApproval && (
<div className="p-6">
<ApprovalCard
request={selectedApproval}
onApprove={() => {
handleApprove(selectedApproval.id)
// 簽核後移到下一個或關閉
if (selectedIndex !== null && selectedIndex < pendingApprovals.length - 1) {
setSelectedIndex(selectedIndex + 1)
} else {
setSelectedIndex(null)
}
}}
onReject={() => {
handleReject(selectedApproval.id)
<ApprovalCard
request={selectedApproval}
onApprove={() => {
handleApprove(selectedApproval.id)
// 簽核後移到下一個或關閉
if (selectedIndex !== null && selectedIndex < pendingApprovals.length - 1) {
setSelectedIndex(selectedIndex + 1)
} else {
setSelectedIndex(null)
}}
holdDuration={1000}
/>
</div>
}
}}
onReject={() => {
handleReject(selectedApproval.id)
setSelectedIndex(null)
}}
holdDuration={1000}
fullHeight
/>
)}
</ApprovalModal>

View File

@@ -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: 左側紅色粗邊框 (乾淨、無光害)

View File

@@ -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 (
<div className="fixed inset-0 z-50 flex items-center justify-center">
// 使用 Portal 渲染到 body確保全屏覆蓋
return createPortal(
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
{/* Backdrop - 深色遮罩,強調聚焦 */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"
@@ -144,10 +154,11 @@ export function ApprovalModal({
</div>
{/* Content - 可滾動 */}
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto p-6">
{children}
</div>
</div>
</div>
</div>,
document.body
)
}