725 lines
26 KiB
TypeScript
725 lines
26 KiB
TypeScript
import React, { useRef, useState } from 'react';
|
|
import { DocumentAST } from '../../types/block';
|
|
import { UserRole } from '../../types/docgov';
|
|
import { canShowDocGovApprovalAction, isUnmanagedDocDuties } from '../../lib/docGovActionVisibility';
|
|
import {
|
|
Send,
|
|
CheckCircle2,
|
|
XCircle,
|
|
Award,
|
|
UserCheck,
|
|
Lock,
|
|
Globe,
|
|
Share2,
|
|
Undo2,
|
|
Shield,
|
|
} from 'lucide-react';
|
|
|
|
interface DocGovActionBarProps {
|
|
ast: DocumentAST;
|
|
currentRole: UserRole;
|
|
currentUserId?: number;
|
|
canOverrideVisibility?: boolean;
|
|
onOpenVisibilityModal?: () => void;
|
|
onClaim: () => void | Promise<void>;
|
|
onSubmitReview: () => void | Promise<void>;
|
|
onPassReview: () => void | Promise<void>;
|
|
onRejectReview: (reason: string) => void | Promise<void>;
|
|
onApprove: () => void | Promise<void>;
|
|
onRejectApprove: (reason: string) => void | Promise<void>;
|
|
onShareToProject?: (docId: string) => void;
|
|
onRevokeShare?: (docId: string) => void;
|
|
onRequestReopen?: () => void;
|
|
}
|
|
|
|
export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
|
ast,
|
|
currentRole,
|
|
currentUserId,
|
|
canOverrideVisibility = true,
|
|
onOpenVisibilityModal,
|
|
onClaim,
|
|
onSubmitReview,
|
|
onPassReview,
|
|
onRejectReview,
|
|
onApprove,
|
|
onRejectApprove,
|
|
onShareToProject,
|
|
onRevokeShare,
|
|
onRequestReopen,
|
|
}) => {
|
|
const [rejectModalOpen, setRejectModalOpen] = useState(false);
|
|
const [rejectReason, setRejectReason] = useState('');
|
|
const [rejectTarget, setRejectTarget] = useState<'review' | 'approve'>('review');
|
|
const [rejectSubmitting, setRejectSubmitting] = useState(false);
|
|
const [rejectError, setRejectError] = useState<string | null>(null);
|
|
const rejectReasonRef = useRef('');
|
|
const rejectTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
|
|
const openReject = (target: 'review' | 'approve') => {
|
|
setRejectTarget(target);
|
|
setRejectReason('');
|
|
rejectReasonRef.current = '';
|
|
setRejectError(null);
|
|
setRejectSubmitting(false);
|
|
setRejectModalOpen(true);
|
|
};
|
|
|
|
/** 优先读 textarea DOM,避免受控 state 与 Playwright fill 竞态导致空理由。 */
|
|
const readRejectReason = (): string => {
|
|
const fromDom = rejectTextareaRef.current?.value ?? '';
|
|
const raw = fromDom || rejectReasonRef.current || rejectReason;
|
|
return raw.trim();
|
|
};
|
|
|
|
const confirmReject = async () => {
|
|
const reason = readRejectReason();
|
|
if (!reason || rejectSubmitting) return;
|
|
setRejectSubmitting(true);
|
|
setRejectError(null);
|
|
try {
|
|
if (rejectTarget === 'review') {
|
|
await Promise.resolve(onRejectReview(reason));
|
|
} else {
|
|
await Promise.resolve(onRejectApprove(reason));
|
|
}
|
|
setRejectModalOpen(false);
|
|
setRejectReason('');
|
|
rejectReasonRef.current = '';
|
|
} catch (err) {
|
|
setRejectError(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setRejectSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const isPrivate = ast.category === 'private';
|
|
const isShared = ast.category === 'shared';
|
|
const isWorkflow = !isPrivate && !isShared;
|
|
const duties = { authorId: ast.authorId, reviewerId: ast.reviewerId, approverId: ast.approverId };
|
|
const unmanagedDuties = isUnmanagedDocDuties(duties);
|
|
const showSubmit = canShowDocGovApprovalAction('submit', currentUserId, duties);
|
|
const showReview = canShowDocGovApprovalAction('review', currentUserId, duties);
|
|
const showApprove = canShowDocGovApprovalAction('approve', currentUserId, duties);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'sticky',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
background: 'var(--bg-panel)',
|
|
borderTop: '1px solid var(--border)',
|
|
padding: '10px 18px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
boxShadow: '0 -4px 16px rgba(0, 0, 0, 0.06)',
|
|
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>
|
|
)}
|
|
|
|
{/* 场景 1: 个人私密文档专属条 */}
|
|
{isPrivate && (
|
|
<>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background: '#FEF3C7',
|
|
color: '#D97706',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
}}
|
|
>
|
|
<Lock size={12} />
|
|
<span>个人私密草稿</span>
|
|
</span>
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
由助手针对您的对话需求生成,<strong>仅您本人可见</strong>。不参与项目受控审签体系。
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
{onShareToProject && (
|
|
<button
|
|
onClick={() => onShareToProject(ast.docId)}
|
|
style={{
|
|
padding: '6px 14px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--brand-500)',
|
|
border: 'none',
|
|
color: '#fff',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '5px',
|
|
cursor: 'pointer',
|
|
boxShadow: '0 2px 6px rgba(79, 107, 255, 0.25)',
|
|
}}
|
|
title="将此推演草稿一键共享给项目组成员查阅与讨论"
|
|
>
|
|
<Share2 size={13} />
|
|
<span>一键共享给项目组</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 场景 2: 项目协作参考资料专属条 */}
|
|
{isShared && (
|
|
<>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background: 'var(--brand-50)',
|
|
color: 'var(--brand-600)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
}}
|
|
>
|
|
<Globe size={12} />
|
|
<span>项目协作参考资料</span>
|
|
</span>
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
共享人: <b>{ast.creatorName || ast.author}</b>
|
|
{ast.sharedAt ? ` (${ast.sharedAt})` : ''} · 项目全员可查阅、引用与批注
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
{/* 仅创建者本人有权取消共享 */}
|
|
{ast.creatorId === currentUserId && onRevokeShare && (
|
|
<button
|
|
onClick={() => onRevokeShare(ast.docId)}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--bg-subtle)',
|
|
border: '1px solid var(--border)',
|
|
color: 'var(--text)',
|
|
fontSize: '12px',
|
|
fontWeight: 500,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '5px',
|
|
cursor: 'pointer',
|
|
}}
|
|
title="撤回该文档的共享状态,恢复为您个人私有"
|
|
>
|
|
<Undo2 size={13} />
|
|
<span>取消共享 (恢复私有)</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 场景 3: 研发体系预设受控交付物 (严格三权分立审签) */}
|
|
{isWorkflow && (
|
|
<>
|
|
{/* 左侧状态标识 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Shield size={14} color="var(--brand-500)" />
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
受控状态:
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background:
|
|
ast.lifecycleState === 'approved'
|
|
? 'var(--success-bg)'
|
|
: ast.lifecycleState === 'reviewing'
|
|
? 'var(--amber-50)'
|
|
: ast.lifecycleState === 'approving'
|
|
? '#F3E8FF'
|
|
: ast.lifecycleState === 'template'
|
|
? '#E0F2FE'
|
|
: 'var(--brand-50)',
|
|
color:
|
|
ast.lifecycleState === 'approved'
|
|
? 'var(--success)'
|
|
: ast.lifecycleState === 'reviewing'
|
|
? 'var(--amber-600)'
|
|
: ast.lifecycleState === 'approving'
|
|
? '#9333EA'
|
|
: ast.lifecycleState === 'template'
|
|
? '#0369A1'
|
|
: 'var(--brand-600)',
|
|
}}
|
|
>
|
|
{ast.lifecycleState === 'approved'
|
|
? '已终审生效'
|
|
: ast.lifecycleState === 'reviewing'
|
|
? '审核中'
|
|
: ast.lifecycleState === 'approving'
|
|
? '批准中'
|
|
: ast.lifecycleState === 'template'
|
|
? '模板预览'
|
|
: '编制中'}
|
|
</span>
|
|
{ast.dutyPolicy === 'author_only' && (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background: 'rgba(79, 107, 255, 0.12)',
|
|
color: 'var(--brand-600)',
|
|
}}
|
|
title="编制、审核、批准由同一人承担;仍保留三步签署"
|
|
>
|
|
编制人兼任
|
|
</span>
|
|
)}
|
|
{ast.isClaimed && (
|
|
<span style={{ fontSize: '11px', color: 'var(--brand-500)', fontWeight: 500 }}>
|
|
• 已被编制人认领
|
|
</span>
|
|
)}
|
|
{ast.isClaimed &&
|
|
currentRole === 'author' &&
|
|
(ast.authorId === undefined || currentUserId === undefined || ast.authorId === currentUserId) && (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background: 'rgba(33, 166, 106, 0.12)',
|
|
color: 'var(--success, #21a66a)',
|
|
maxWidth: '280px',
|
|
lineHeight: 1.35,
|
|
}}
|
|
title="认领后他人发起的修改需您确认"
|
|
>
|
|
您已于{' '}
|
|
{ast.authorClaimedAt
|
|
? new Date(ast.authorClaimedAt).toLocaleString('zh-CN', {
|
|
month: 'numeric',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
: '此前'}{' '}
|
|
认领本文档;此后他人发起的修改需您确认。
|
|
</span>
|
|
)}
|
|
{ast.visibilityRule && ast.visibilityRule.mode !== 'inherit' && (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
padding: '2px 8px',
|
|
borderRadius: 'var(--r-pill)',
|
|
background:
|
|
ast.visibilityRule.mode === 'designated_only'
|
|
? 'rgba(239, 68, 68, 0.12)'
|
|
: ast.visibilityRule.mode === 'role_whitelist'
|
|
? 'rgba(245, 158, 11, 0.12)'
|
|
: 'rgba(34, 197, 94, 0.12)',
|
|
color:
|
|
ast.visibilityRule.mode === 'designated_only'
|
|
? 'var(--danger)'
|
|
: ast.visibilityRule.mode === 'role_whitelist'
|
|
? 'var(--amber-600)'
|
|
: 'var(--success)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
}}
|
|
title={`覆盖原因: ${ast.visibilityRule.overrideReason || '无'}`}
|
|
>
|
|
<Lock size={10} />
|
|
<span>
|
|
{ast.visibilityRule.mode === 'designated_only'
|
|
? '仅责任人可见'
|
|
: ast.visibilityRule.mode === 'role_whitelist'
|
|
? '角色白名单'
|
|
: '全员公开'}
|
|
</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 右侧动作流转按钮 */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
{onOpenVisibilityModal && (
|
|
<button
|
|
onClick={canOverrideVisibility ? onOpenVisibilityModal : undefined}
|
|
disabled={!canOverrideVisibility}
|
|
style={{
|
|
padding: '5px 10px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background:
|
|
ast.visibilityRule && ast.visibilityRule.mode !== 'inherit'
|
|
? 'rgba(245, 158, 11, 0.12)'
|
|
: 'var(--bg-subtle)',
|
|
border:
|
|
ast.visibilityRule && ast.visibilityRule.mode !== 'inherit'
|
|
? '1px solid var(--amber-500)'
|
|
: '1px solid var(--border)',
|
|
color:
|
|
ast.visibilityRule && ast.visibilityRule.mode !== 'inherit'
|
|
? 'var(--amber-600)'
|
|
: 'var(--text-muted)',
|
|
fontSize: '11.5px',
|
|
fontWeight: 500,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
cursor: canOverrideVisibility ? 'pointer' : 'not-allowed',
|
|
opacity: canOverrideVisibility ? 1 : 0.6,
|
|
}}
|
|
title={
|
|
canOverrideVisibility
|
|
? '配置该文档的独立可见性覆盖策略'
|
|
: '当前角色无权配置单文档可见性覆盖'
|
|
}
|
|
>
|
|
<Lock size={12} />
|
|
<span>
|
|
{ast.visibilityRule && ast.visibilityRule.mode !== 'inherit'
|
|
? '已设权限覆盖'
|
|
: '权限覆盖'}
|
|
</span>
|
|
</button>
|
|
)}
|
|
{/* 1. 编制阶段 */}
|
|
{ast.lifecycleState === 'drafting' && (
|
|
<>
|
|
{unmanagedDuties ? (
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
该文档未纳入人工审批,不提供送审、审核或批准
|
|
</span>
|
|
) : showSubmit ? (
|
|
<>
|
|
{!ast.isClaimed ? (
|
|
<button
|
|
onClick={onClaim}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--bg-subtle)',
|
|
border: '1px solid var(--border)',
|
|
color: 'var(--text)',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '5px',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<UserCheck size={13} style={{ color: 'var(--brand-500)' }} />
|
|
<span>认领此文档</span>
|
|
</button>
|
|
) : null}
|
|
|
|
<button
|
|
onClick={onSubmitReview}
|
|
style={{
|
|
padding: '6px 14px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--brand-grad)',
|
|
border: 'none',
|
|
color: '#fff',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '5px',
|
|
cursor: 'pointer',
|
|
boxShadow: '0 2px 6px rgba(79, 107, 255, 0.25)',
|
|
}}
|
|
>
|
|
<Send size={13} />
|
|
<span>提交送审 (流转至审核人)</span>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
等待编制负责人({ast.author})完成初稿并提交送审
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 2. 审核阶段 */}
|
|
{ast.lifecycleState === 'reviewing' && (
|
|
<>
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
color: 'var(--text-faint)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
}}
|
|
>
|
|
<Lock size={12} /> 文档已锁定
|
|
</span>
|
|
{showReview ? (
|
|
<>
|
|
<button
|
|
onClick={() => openReject('review')}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--danger-bg)',
|
|
border: '1px solid var(--danger)',
|
|
color: 'var(--danger)',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<XCircle size={13} />
|
|
<span>驳回修改</span>
|
|
</button>
|
|
<button
|
|
onClick={onPassReview}
|
|
style={{
|
|
padding: '6px 14px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--amber-500)',
|
|
border: 'none',
|
|
color: '#fff',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
cursor: 'pointer',
|
|
boxShadow: '0 2px 6px rgba(242, 169, 59, 0.3)',
|
|
}}
|
|
>
|
|
<CheckCircle2 size={13} />
|
|
<span>审核通过 (送呈批准人)</span>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
等待审核负责人({ast.reviewer})完成审核签署
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 3. 批准阶段 */}
|
|
{ast.lifecycleState === 'approving' && (
|
|
<>
|
|
{showApprove ? (
|
|
<>
|
|
<button
|
|
onClick={() => openReject('approve')}
|
|
style={{
|
|
padding: '6px 12px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--danger-bg)',
|
|
border: '1px solid var(--danger)',
|
|
color: 'var(--danger)',
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
<XCircle size={13} />
|
|
<span>驳回修改</span>
|
|
</button>
|
|
<button
|
|
onClick={onApprove}
|
|
style={{
|
|
padding: '6px 16px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--success)',
|
|
border: 'none',
|
|
color: '#fff',
|
|
fontSize: '12.5px',
|
|
fontWeight: 700,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '5px',
|
|
cursor: 'pointer',
|
|
boxShadow: '0 2px 8px rgba(33, 166, 106, 0.35)',
|
|
}}
|
|
>
|
|
<Award size={14} />
|
|
<span>终审批准生效 (自动入标杆库)</span>
|
|
</button>
|
|
</>
|
|
) : (
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
等待批准负责人({ast.approver})终审批准发布
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 4. 已批准终态 */}
|
|
{ast.lifecycleState === 'approved' && (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
color: 'var(--success)',
|
|
fontSize: '12.5px',
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
<Award size={16} />
|
|
<span>受控归档交付物 (已沉淀为全局标杆经验)</span>
|
|
{onRequestReopen &&
|
|
(currentUserId === ast.authorId ||
|
|
currentUserId === ast.approverId ||
|
|
currentUserId === ast.creatorId) && (
|
|
<button
|
|
onClick={onRequestReopen}
|
|
style={{
|
|
marginLeft: '8px',
|
|
padding: '5px 10px',
|
|
borderRadius: 'var(--r-sm)',
|
|
background: 'var(--bg-subtle)',
|
|
border: '1px solid var(--border)',
|
|
color: 'var(--text)',
|
|
fontSize: '11.5px',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
}}
|
|
title="向原批准人申请将文档重开至编制期"
|
|
>
|
|
申请再修订
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|