fix(web): portal reject modal above stacking context

This commit is contained in:
weichen
2026-09-26 07:48:30 +00:00
parent 157ec407e2
commit 887a75c1b1
+114 -99
View File
@@ -1,4 +1,5 @@
import React, { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { DocumentAST } from '../../types/block';
import { UserRole } from '../../types/docgov';
import { canShowDocGovApprovalAction, isUnmanagedDocDuties } from '../../lib/docGovActionVisibility';
@@ -102,6 +103,118 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
const showReview = canShowDocGovApprovalAction('review', currentUserId, duties);
const showApprove = canShowDocGovApprovalAction('approve', currentUserId, duties);
// Portal to document.body so sticky ActionBar stacking context cannot trap the overlay
// below chat floating chrome (zIndex must match DocVisibilityModal ≥ 1000).
const rejectModal =
rejectModalOpen && typeof document !== 'undefined'
? createPortal(
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
width: '420px',
background: 'var(--bg-panel)',
borderRadius: 'var(--r-md)',
border: '1px solid var(--border)',
padding: '16px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
boxShadow: 'var(--shadow-lg)',
}}
>
<div style={{ fontSize: '14px', fontWeight: 700, color: 'var(--danger)' }}>
请填写法定驳回原因 (必填)
</div>
<textarea
ref={rejectTextareaRef}
value={rejectReason}
onChange={(e) => {
const next = e.target.value;
rejectReasonRef.current = next;
setRejectReason(next);
}}
onKeyDown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
void confirmReject();
}
}}
placeholder="说明驳回修改的具体意见,例如:缺少第3章电气安全检验标准,或技术参数指标不符合临床预期..."
rows={4}
disabled={rejectSubmitting}
style={{
width: '100%',
padding: '8px 10px',
borderRadius: 'var(--r-sm)',
border: '1px solid var(--border)',
background: 'var(--bg-subtle)',
color: 'var(--text)',
fontSize: '12.5px',
resize: 'none',
}}
/>
{rejectError ? (
<div style={{ fontSize: '12px', color: 'var(--danger)' }} role="alert">
驳回失败:{rejectError}
</div>
) : null}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
type="button"
onClick={() => {
if (rejectSubmitting) return;
setRejectModalOpen(false);
}}
disabled={rejectSubmitting}
style={{
padding: '6px 12px',
borderRadius: 'var(--r-sm)',
background: 'var(--bg-subtle)',
border: '1px solid var(--border)',
color: 'var(--text)',
cursor: rejectSubmitting ? 'not-allowed' : 'pointer',
opacity: rejectSubmitting ? 0.6 : 1,
}}
>
取消
</button>
<button
type="button"
data-testid="docgov-reject-confirm"
onClick={() => {
void confirmReject();
}}
disabled={rejectSubmitting}
style={{
padding: '6px 14px',
borderRadius: 'var(--r-sm)',
background: 'var(--danger)',
border: 'none',
color: '#fff',
fontWeight: 600,
opacity: rejectSubmitting ? 0.5 : 1,
cursor: rejectSubmitting ? 'not-allowed' : 'pointer',
}}
>
{rejectSubmitting ? '驳回中…' : '确认驳回退回编制'}
</button>
</div>
</div>
</div>,
document.body,
)
: null;
return (
<div
style={{
@@ -119,105 +232,7 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
zIndex: 30,
}}
>
{/* 驳回理由弹窗 */}
{rejectModalOpen && (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.5)',
zIndex: 110,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
width: '420px',
background: 'var(--bg-panel)',
borderRadius: 'var(--r-md)',
border: '1px solid var(--border)',
padding: '16px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
boxShadow: 'var(--shadow-lg)',
}}
>
<div style={{ fontSize: '14px', fontWeight: 700, color: 'var(--danger)' }}>
请填写法定驳回原因 (必填)
</div>
<textarea
ref={rejectTextareaRef}
value={rejectReason}
onChange={(e) => {
const next = e.target.value;
rejectReasonRef.current = next;
setRejectReason(next);
}}
placeholder="说明驳回修改的具体意见,例如:缺少第3章电气安全检验标准,或技术参数指标不符合临床预期..."
rows={4}
disabled={rejectSubmitting}
style={{
width: '100%',
padding: '8px 10px',
borderRadius: 'var(--r-sm)',
border: '1px solid var(--border)',
background: 'var(--bg-subtle)',
color: 'var(--text)',
fontSize: '12.5px',
resize: 'none',
}}
/>
{rejectError ? (
<div style={{ fontSize: '12px', color: 'var(--danger)' }} role="alert">
驳回失败:{rejectError}
</div>
) : null}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
type="button"
onClick={() => {
if (rejectSubmitting) return;
setRejectModalOpen(false);
}}
disabled={rejectSubmitting}
style={{
padding: '6px 12px',
borderRadius: 'var(--r-sm)',
background: 'var(--bg-subtle)',
border: '1px solid var(--border)',
color: 'var(--text)',
cursor: rejectSubmitting ? 'not-allowed' : 'pointer',
opacity: rejectSubmitting ? 0.6 : 1,
}}
>
取消
</button>
<button
type="button"
onClick={() => {
void confirmReject();
}}
disabled={rejectSubmitting}
style={{
padding: '6px 14px',
borderRadius: 'var(--r-sm)',
background: 'var(--danger)',
border: 'none',
color: '#fff',
fontWeight: 600,
opacity: rejectSubmitting ? 0.5 : 1,
cursor: rejectSubmitting ? 'not-allowed' : 'pointer',
}}
>
{rejectSubmitting ? '驳回中…' : '确认驳回退回编制'}
</button>
</div>
</div>
</div>
)}
{rejectModal}
{/* 场景 1: 个人私密文档专属条 */}
{isPrivate && (