fix(web): portal reject modal above stacking context
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user