diff --git a/web/src/App.tsx b/web/src/App.tsx index ac511dbe..4986ad6b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -11,6 +11,7 @@ import { DocGovActionBar } from './components/doc/DocGovActionBar'; import { ProposalConfirmCard } from './components/doc/ProposalConfirmCard'; import { UserProjectRightsModal } from './components/user/UserProjectRightsModal'; import { SettingsModal } from './components/user/SettingsModal'; +import { DocVisibilityModal } from './components/doc/DocVisibilityModal'; import { markdownToBlockAST } from './engine/markdownToBlock'; import { blockASTToMarkdown } from './engine/blockToMarkdown'; import { recomputeNumbering } from './engine/autoNumber'; @@ -23,6 +24,12 @@ import { InteractiveCardData, } from './types/workbench'; import { UserRole, UserProfile, DocPermissionRole } from './types/docgov'; +import { RoleDefinition, DocVisibilityRule } from './types/rbac'; +import { + DEFAULT_ROLES, + hasPermission, + filterVisibleDeliverablesWithRBAC, +} from './engine/rbac'; import { MOCK_DOCUMENTS } from './data/mockDocuments'; import { exportDocumentToDocx } from './engine/docxRenderer'; import { exportDocumentToXlsx } from './engine/xlsxRenderer'; @@ -47,6 +54,10 @@ import { saveMessages, loadDeliverables, saveDeliverables, + loadRoles, + saveRoles, + loadUsers, + saveUsers, } from './data/storage'; export const INITIAL_DELIVERABLES: DeliverableItem[] = [ @@ -207,6 +218,7 @@ export const MOCK_USERS: UserProfile[] = [ employeeId: 'SMDF-0821', department: '机器人与微创内窥镜研发中心', position: '资深医疗器械方案工程师', + roleId: 'role-engineer', projectPermissions: { 'doc-001': 'author', // 编制人 'doc-002': 'reviewer', // 审核人 @@ -222,6 +234,7 @@ export const MOCK_USERS: UserProfile[] = [ employeeId: 'SMDF-0205', department: '机器人与微创内窥镜研发中心', position: '研发项目技术主管', + roleId: 'role-tech-lead', projectPermissions: { 'doc-001': 'reviewer', 'doc-002': 'author', @@ -237,6 +250,7 @@ export const MOCK_USERS: UserProfile[] = [ employeeId: 'SMDF-0018', department: '医疗器械研发事业部', position: '微创器械研发总监', + roleId: 'role-director', projectPermissions: { 'doc-001': 'approver', 'doc-002': 'approver', @@ -245,6 +259,28 @@ export const MOCK_USERS: UserProfile[] = [ 'doc-shared-001': 'approver', }, }, + { + id: 4, + name: '赵管理员', + employeeId: 'SMDF-0001', + department: '系统安全与技术委员会', + position: '系统全域管理总管', + roleId: 'role-admin', + projectPermissions: { + 'doc-001': 'approver', + 'doc-002': 'approver', + 'doc-003': 'approver', + }, + }, + { + id: 5, + name: '钱审计', + employeeId: 'SMDF-9901', + department: '第三方合规审计与认证机构', + position: '外部合规审查专员', + roleId: 'role-guest', + projectPermissions: {}, + }, ]; export default function App() { @@ -261,8 +297,21 @@ export default function App() { saveActiveSessionId(activeSessionId); }, [activeSessionId]); - // 当前用户身份 (职位在全公司唯一不变,但在不同文档有不同法定职责) - const [currentUser, setCurrentUser] = useState(MOCK_USERS[0]); + // 全局角色库与人员名单状态 (支持动态增删改与持久化) + const [roles, setRoles] = useState(() => loadRoles(DEFAULT_ROLES)); + const [allUsers, setAllUsers] = useState(() => loadUsers(MOCK_USERS)); + + // 自动持久化 roles 与 allUsers + useEffect(() => { + saveRoles(roles); + }, [roles]); + + useEffect(() => { + saveUsers(allUsers); + }, [allUsers]); + + // 当前用户身份 (职位在全公司唯一不变,绑定全局角色,但在不同文档有不同法定职责) + const [currentUser, setCurrentUser] = useState(() => allUsers[0] || MOCK_USERS[0]); // 全生命周期完整交付物大盘清单 (含工作流受控、项目协作共享、个人私密生成三大分类,支持持久化) const [deliverables, setDeliverables] = useState(() => @@ -281,6 +330,24 @@ export default function App() { // 模态浮层控制 const [rightsModalOpen, setRightsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false); + const [docVisibilityModalOpen, setDocVisibilityModalOpen] = useState(false); + const [targetDocForVisibility, setTargetDocForVisibility] = useState(null); + + // 核心功能权限计算 + const canCreateProject = useMemo(() => { + return hasPermission(currentUser, roles, 'project:create'); + }, [currentUser, roles]); + + const canAssignLead = useMemo(() => { + return hasPermission(currentUser, roles, 'project:assign_lead'); + }, [currentUser, roles]); + + const canOverrideVisibility = useMemo(() => { + return ( + hasPermission(currentUser, roles, 'doc:override_visibility') || + hasPermission(currentUser, roles, 'perm:manage') + ); + }, [currentUser, roles]); // 获取当前激活会话的项目ID (默认为 1) const currentProjectId = useMemo(() => { @@ -293,15 +360,27 @@ export default function App() { return deliverables.filter((doc) => doc.projectId === currentProjectId); }, [deliverables, currentProjectId]); - // 基于当前登录用户过滤可见交付物 (私密文档严格按 creatorId 隔离,流程与共享文档全员可见) + // 基于当前登录用户与 RBAC 规则过滤可见交付物 (私密文档严格按 creatorId 隔离,单文档覆盖优先于工作流基线) const visibleDeliverables = useMemo(() => { - return currentProjectDeliverables.filter((doc) => { - if (doc.category === 'workflow' || doc.category === 'shared') { - return true; - } - return doc.creatorId === currentUser.id; + return filterVisibleDeliverablesWithRBAC(currentProjectDeliverables, currentUser, roles); + }, [currentProjectDeliverables, currentUser, roles]); + + // 单文档可见性覆盖保存处理 + const handleSaveDocVisibilityRule = (docId: string, rule: DocVisibilityRule | undefined) => { + setDeliverables((prev) => + prev.map((d) => (d.docId === docId ? { ...d, visibilityRule: rule } : d)) + ); + setDocCache((prev) => { + if (!prev[docId]) return prev; + return { + ...prev, + [docId]: { + ...prev[docId], + visibilityRule: rule, + }, + }; }); - }, [currentProjectDeliverables, currentUser.id]); + }; // 当用户切换或可见列表变动时,自动校准激活文档 useEffect(() => { @@ -1209,6 +1288,7 @@ export default function App() { setActiveDocId(clonedDocs[0].docId); setActiveCategory('workflow'); }} + canCreateProject={canCreateProject} currentUser={currentUser} currentRole={currentDocRole} currentUserName={currentUser.name} @@ -1266,6 +1346,10 @@ export default function App() { onShareToProject={handleShareToProject} onRevokeShare={handleRevokeShare} onDeleteDoc={handleDeleteDoc} + onOpenDocVisibility={(doc) => { + setTargetDocForVisibility(doc); + setDocVisibilityModalOpen(true); + }} /> {/* 块状文档渲染器 */} @@ -1290,6 +1374,12 @@ export default function App() { ast={docAST} currentRole={currentDocRole} currentUserId={currentUser.id} + canOverrideVisibility={canOverrideVisibility} + onOpenVisibilityModal={() => { + const currentDoc = deliverables.find((d) => d.docId === activeDocId) || null; + setTargetDocForVisibility(currentDoc); + setDocVisibilityModalOpen(true); + }} onClaim={handleClaim} onSubmitReview={handleSubmitReview} onPassReview={handlePassReview} @@ -1331,16 +1421,46 @@ export default function App() { currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'} deliverables={visibleDeliverables} activeDocId={activeDocId} - allUsers={MOCK_USERS} - onSwitchUser={(u) => setCurrentUser(u)} + allUsers={allUsers} + canAssignLead={canAssignLead} + onSwitchUser={(u) => { + const fresh = allUsers.find((x) => x.id === u.id) || u; + setCurrentUser(fresh); + }} onSelectDoc={handleSelectDoc} onRoleChangeForDoc={handleRoleChangeForDoc} /> - {/* 预留系统设置与管理员后台弹窗 */} + {/* 系统权限与角色管理控制台 */} setSettingsModalOpen(false)} + roles={roles} + onSaveRoles={(newRoles) => setRoles(newRoles)} + users={allUsers} + currentUser={currentUser} + onUpdateUserRole={(userId, roleId) => { + setAllUsers((prev) => + prev.map((u) => (u.id === userId ? { ...u, roleId } : u)) + ); + if (currentUser.id === userId) { + setCurrentUser((prev) => ({ ...prev, roleId })); + } + }} + onSwitchUser={(newUser) => { + const fresh = allUsers.find((u) => u.id === newUser.id) || newUser; + setCurrentUser(fresh); + }} + /> + + {/* 单文档可见性覆盖弹窗 */} + setDocVisibilityModalOpen(false)} + doc={targetDocForVisibility} + roles={roles} + currentUserName={currentUser.name} + onSaveRule={handleSaveDocVisibilityRule} /> ); diff --git a/web/src/components/layout/AppShell.tsx b/web/src/components/layout/AppShell.tsx index b97c262f..b77293cf 100644 --- a/web/src/components/layout/AppShell.tsx +++ b/web/src/components/layout/AppShell.tsx @@ -12,6 +12,7 @@ interface AppShellProps { activeSessionId: string; onSelectSession: (id: string) => void; onNewSession: () => void; + canCreateProject?: boolean; currentUser: UserProfile; currentRole: UserRole; currentUserName: string; @@ -27,6 +28,7 @@ export const AppShell: React.FC = ({ activeSessionId, onSelectSession, onNewSession, + canCreateProject = true, currentUser, currentRole, currentUserName: _currentUserName, @@ -111,6 +113,7 @@ export const AppShell: React.FC = ({ activeSessionId={activeSessionId} onSelectSession={onSelectSession} onNewSession={onNewSession} + canCreateProject={canCreateProject} currentUser={currentUser} currentRole={currentRole} onOpenRightsModal={onOpenRightsModal} diff --git a/web/src/components/layout/LeftRail.tsx b/web/src/components/layout/LeftRail.tsx index 4913ecee..62b31ec2 100644 --- a/web/src/components/layout/LeftRail.tsx +++ b/web/src/components/layout/LeftRail.tsx @@ -10,6 +10,7 @@ interface LeftRailProps { activeSessionId: string; onSelectSession: (sessionId: string) => void; onNewSession: () => void; + canCreateProject?: boolean; currentUser: UserProfile; currentRole: UserRole; onOpenRightsModal: () => void; @@ -23,6 +24,7 @@ export const LeftRail: React.FC = ({ activeSessionId, onSelectSession, onNewSession, + canCreateProject = true, currentUser, currentRole, onOpenRightsModal, @@ -65,22 +67,31 @@ export const LeftRail: React.FC = ({ {/* 顶部操作区 */}