feat(web): add doc visibility override modal and badges in deck drawer
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DeliverableItem, DeliverableCategory } from '../../types/workbench';
|
||||
import { DocLifecycleState } from '../../types/docgov';
|
||||
import { DocVisibilityRule } from '../../types/rbac';
|
||||
import {
|
||||
X,
|
||||
CheckCircle2,
|
||||
@@ -28,6 +29,7 @@ interface DeckDrawerProps {
|
||||
onShareToProject?: (docId: string) => void;
|
||||
onRevokeShare?: (docId: string) => void;
|
||||
onDeleteDoc?: (docId: string) => void;
|
||||
onOpenDocVisibility?: (doc: DeliverableItem) => void;
|
||||
}
|
||||
|
||||
export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
@@ -40,6 +42,7 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
onShareToProject,
|
||||
onRevokeShare,
|
||||
onDeleteDoc,
|
||||
onOpenDocVisibility,
|
||||
}) => {
|
||||
const [filterCategory, setFilterCategory] = useState<'all' | DeliverableCategory>('all');
|
||||
|
||||
@@ -180,6 +183,40 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderVisibilityBadge = (rule?: DocVisibilityRule) => {
|
||||
if (!rule || rule.mode === 'inherit') return null;
|
||||
const isDesignated = rule.mode === 'designated_only';
|
||||
const isWhitelist = rule.mode === 'role_whitelist';
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10.5px',
|
||||
fontWeight: 600,
|
||||
padding: '1px 6px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
background: isDesignated
|
||||
? 'rgba(239, 68, 68, 0.12)'
|
||||
: isWhitelist
|
||||
? 'rgba(245, 158, 11, 0.12)'
|
||||
: 'rgba(34, 197, 94, 0.12)',
|
||||
color: isDesignated
|
||||
? 'var(--danger)'
|
||||
: isWhitelist
|
||||
? 'var(--amber-600)'
|
||||
: 'var(--success)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
title={`覆盖原因: ${rule.overrideReason || '权限覆盖模式'}`}
|
||||
>
|
||||
<Lock size={10} />
|
||||
<span>{isDesignated ? '仅责任人' : isWhitelist ? '指定角色' : '公开'}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const stages: { stage: string; title: string }[] = [
|
||||
{ stage: 'V0.1', title: 'V0.1 需求摸底与产品定义' },
|
||||
{ stage: 'V0.5', title: 'V0.5 方案成型与法规对齐' },
|
||||
@@ -411,7 +448,10 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
{renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
{renderVisibilityBadge(doc.visibilityRule)}
|
||||
{renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
|
||||
@@ -18,6 +18,8 @@ interface DocGovActionBarProps {
|
||||
ast: DocumentAST;
|
||||
currentRole: UserRole;
|
||||
currentUserId?: number;
|
||||
canOverrideVisibility?: boolean;
|
||||
onOpenVisibilityModal?: () => void;
|
||||
onClaim: () => void;
|
||||
onSubmitReview: () => void;
|
||||
onPassReview: () => void;
|
||||
@@ -32,6 +34,8 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
ast,
|
||||
currentRole,
|
||||
currentUserId,
|
||||
canOverrideVisibility = true,
|
||||
onOpenVisibilityModal,
|
||||
onClaim,
|
||||
onSubmitReview,
|
||||
onPassReview,
|
||||
@@ -315,10 +319,86 @@ export const DocGovActionBar: React.FC<DocGovActionBarProps> = ({
|
||||
• 已被编制人认领
|
||||
</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' && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import {
|
||||
X,
|
||||
ShieldAlert,
|
||||
Lock,
|
||||
Globe,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { DeliverableItem } from '../../types/workbench';
|
||||
import { RoleDefinition, DocVisibilityRule, VisibilityMode } from '../../types/rbac';
|
||||
|
||||
export interface DocVisibilityModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
doc: DeliverableItem | null;
|
||||
roles: RoleDefinition[];
|
||||
currentUserName: string;
|
||||
onSaveRule: (docId: string, rule: DocVisibilityRule | undefined) => void;
|
||||
}
|
||||
|
||||
export const DocVisibilityModal: FC<DocVisibilityModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
doc,
|
||||
roles,
|
||||
currentUserName,
|
||||
onSaveRule,
|
||||
}) => {
|
||||
const [mode, setMode] = useState<VisibilityMode>('inherit');
|
||||
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
|
||||
const [overrideReason, setOverrideReason] = useState<string>('');
|
||||
|
||||
// 初始化已有规则
|
||||
useEffect(() => {
|
||||
if (doc?.visibilityRule) {
|
||||
setMode(doc.visibilityRule.mode);
|
||||
setSelectedRoleIds(doc.visibilityRule.allowedRoleIds || []);
|
||||
setOverrideReason(doc.visibilityRule.overrideReason || '');
|
||||
} else {
|
||||
setMode('inherit');
|
||||
setSelectedRoleIds([]);
|
||||
setOverrideReason('');
|
||||
}
|
||||
}, [doc, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen || !doc) return null;
|
||||
|
||||
const handleToggleRole = (roleId: string) => {
|
||||
setSelectedRoleIds((prev) =>
|
||||
prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (mode === 'inherit') {
|
||||
onSaveRule(doc.docId, undefined);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const rule: DocVisibilityRule = {
|
||||
mode,
|
||||
allowedRoleIds: mode === 'role_whitelist' ? selectedRoleIds : undefined,
|
||||
overrideReason: overrideReason.trim() || '业务主管根据合规与保密要求设置覆盖策略',
|
||||
configuredBy: currentUserName,
|
||||
configuredAt: new Date().toLocaleString(),
|
||||
};
|
||||
|
||||
onSaveRule(doc.docId, rule);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleResetToInherit = () => {
|
||||
onSaveRule(doc.docId, undefined);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(11, 14, 20, 0.75)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '560px',
|
||||
maxWidth: '95vw',
|
||||
backgroundColor: 'var(--bg-panel, #131820)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--border)',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
animation: 'scaleIn 0.18s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* 顶部标题栏 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '14px 20px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '34px',
|
||||
height: '34px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.15)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--amber-500)',
|
||||
}}
|
||||
>
|
||||
<Lock size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
单文档可见性覆盖策略 (Doc Visibility Override)
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
设置后将优先于工作流默认可见性规则生效
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--text-muted)',
|
||||
padding: '6px',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 主体表单 */}
|
||||
<div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
{/* 目标文档信息卡 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 14px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--bg-subtle)',
|
||||
border: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<FileText size={14} color="var(--brand-500)" />
|
||||
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
<span>所属阶段: <b>{doc.stage}</b></span>
|
||||
<span>编制: <b>{doc.author}</b></span>
|
||||
<span>审核: <b>{doc.reviewer}</b></span>
|
||||
<span>批准: <b>{doc.approver}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 覆盖模式选择 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
选择可见性覆盖策略:
|
||||
</span>
|
||||
|
||||
{/* 模式 1: 继承工作流默认 */}
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '6px',
|
||||
border: mode === 'inherit' ? '1.5px solid var(--brand-500)' : '1px solid var(--border)',
|
||||
backgroundColor: mode === 'inherit' ? 'rgba(79, 107, 255, 0.06)' : 'var(--bg-subtle)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="visibilityMode"
|
||||
checked={mode === 'inherit'}
|
||||
onChange={() => setMode('inherit')}
|
||||
style={{ marginTop: '2px' }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
1. 继承工作流默认规则 (默认)
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
不进行单文档覆盖。具备常规工作流查阅权(<code>workflow:view_all</code>)的项目团队成员均可查阅。
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 模式 2: 仅三权责任人可见 */}
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '6px',
|
||||
border: mode === 'designated_only' ? '1.5px solid var(--danger)' : '1px solid var(--border)',
|
||||
backgroundColor: mode === 'designated_only' ? 'rgba(239, 68, 68, 0.06)' : 'var(--bg-subtle)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="visibilityMode"
|
||||
checked={mode === 'designated_only'}
|
||||
onChange={() => setMode('designated_only')}
|
||||
style={{ marginTop: '2px' }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--danger)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<Lock size={13} />
|
||||
<span>2. 严格收紧:仅文档三权责任人可见</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
绝密涉密文档。仅该文档当前指定的【编制人 ({doc.author})】、【审核人 ({doc.reviewer})】、【批准人 ({doc.approver})】有权查阅,项目其他成员(包括普通主管)一律隐藏不可见。
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 模式 3: 指定全局角色白名单 */}
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '6px',
|
||||
border: mode === 'role_whitelist' ? '1.5px solid var(--amber-500)' : '1px solid var(--border)',
|
||||
backgroundColor: mode === 'role_whitelist' ? 'rgba(245, 158, 11, 0.06)' : 'var(--bg-subtle)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="visibilityMode"
|
||||
checked={mode === 'role_whitelist'}
|
||||
onChange={() => setMode('role_whitelist')}
|
||||
style={{ marginTop: '2px' }}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--amber-600)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<Users size={13} />
|
||||
<span>3. 角色白名单:仅指定全局角色可见</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
仅勾选的全局角色成员可查阅(法定三权责任人依然受保底保护)。
|
||||
</div>
|
||||
|
||||
{mode === 'role_whitelist' && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'var(--bg-card)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
{roles.map((r) => {
|
||||
const isChecked = selectedRoleIds.includes(r.id);
|
||||
return (
|
||||
<label
|
||||
key={r.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: '11.5px',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: isChecked ? 'rgba(79, 107, 255, 0.15)' : 'transparent',
|
||||
color: isChecked ? 'var(--brand-500)' : 'var(--text)',
|
||||
border: isChecked ? '1px solid var(--brand-500)' : '1px solid var(--border-light)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => handleToggleRole(r.id)}
|
||||
/>
|
||||
<span>{r.name}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* 模式 4: 全员公开 */}
|
||||
<label
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '6px',
|
||||
border: mode === 'public' ? '1.5px solid var(--success)' : '1px solid var(--border)',
|
||||
backgroundColor: mode === 'public' ? 'rgba(34, 197, 94, 0.06)' : 'var(--bg-subtle)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="visibilityMode"
|
||||
checked={mode === 'public'}
|
||||
onChange={() => setMode('public')}
|
||||
style={{ marginTop: '2px' }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600, color: 'var(--success)', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<Globe size={13} />
|
||||
<span>4. 放宽公开:全员公开查阅 (包括访客与外部审计)</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '2px' }}>
|
||||
如产品公开规格白皮书,即使无常规工作流查阅权的普通访客也可查阅。
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 覆盖原因留痕输入 */}
|
||||
{mode !== 'inherit' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<span style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
覆盖原因与合规留痕说明:
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="例如: 涉及未公开核心光学算法与敏感供应链成本,按ISO13485要求受控锁定"
|
||||
value={overrideReason}
|
||||
onChange={(e) => setOverrideReason(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
fontSize: '12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-panel)',
|
||||
color: 'var(--text-strong)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部操作栏 */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{doc.visibilityRule && doc.visibilityRule.mode !== 'inherit' ? (
|
||||
<button
|
||||
onClick={handleResetToInherit}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--bg-panel)',
|
||||
color: 'var(--text-muted)',
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
清除覆盖 (恢复继承)
|
||||
</button>
|
||||
) : (
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
当前状态:继承工作流默认可见性
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'transparent',
|
||||
color: 'var(--text-muted)',
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={{
|
||||
padding: '6px 18px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'var(--brand-grad)',
|
||||
color: '#fff',
|
||||
fontSize: '12.5px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
保存并应用覆盖
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DeliverableCategory } from './workbench';
|
||||
import { DocLifecycleState } from './docgov';
|
||||
import { DocVisibilityRule } from './rbac';
|
||||
|
||||
export type BlockType =
|
||||
| 'document' // 根文档块 (Root Container)
|
||||
@@ -62,6 +63,7 @@ export interface DocumentAST {
|
||||
reviewer: string;
|
||||
approver: string;
|
||||
summary?: string;
|
||||
visibilityRule?: DocVisibilityRule;
|
||||
updatedAt: string;
|
||||
rootBlock: DocumentBlock;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user