From 16674d476c2c7e9de1e5b878870f859aca73cef0 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:27:17 +0800 Subject: [PATCH 01/18] docs(spec): add dynamic roles and doc visibility rbac design spec Co-authored-by: Cursor --- ...ic-roles-and-doc-visibility-rbac-design.md | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-08-dynamic-roles-and-doc-visibility-rbac-design.md diff --git a/docs/superpowers/specs/2026-09-08-dynamic-roles-and-doc-visibility-rbac-design.md b/docs/superpowers/specs/2026-09-08-dynamic-roles-and-doc-visibility-rbac-design.md new file mode 100644 index 00000000..6658e071 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-dynamic-roles-and-doc-visibility-rbac-design.md @@ -0,0 +1,258 @@ +# 动态角色管理与单文档可见性覆盖权限系统架构设计规范 +# (Dynamic Roles & Document-Level Visibility Override RBAC Specification) + +- **归档路径**: `docs/superpowers/specs/2026-09-08-dynamic-roles-and-doc-visibility-rbac-design.md` +- **编制日期**: 2026-09-08 +- **当前状态**: 批准实施 (Approved) +- **技术栈**: React 19 + TypeScript + Vite + FlowMind Token 设计系统 + Go 后端规范兼容 + +--- + +## 一、 业务背景与问题分析 (Background & Core Problem) + +在医疗器械全流程智能协同研发系统中,业务交付物既包含依照法规(GMP/ISO13485/NMPA)强制要求的受控主线文档(如《立项可行性报告》、《技术规格书》),也包含高敏感度的商业机密、专利拆解草案以及跨部门协作资料。 + +目前系统已具备初步的三层交付物分类(`workflow` / `shared` / `private`)与文档三权(编制/审核/批准)生命周期流转。但在团队规模化协作和企业级权限管控上面临以下核心痛点: + +1. **角色模型固化且缺乏全局唯一性**: + - 用户角色与项目职责混杂,缺乏公司级/系统级的全局角色(如研发总监、技术主管、合规专家、方案工程师、外聘访客); + - 无法在系统中动态新增、修改或删除角色,无法根据企业实际组织架构灵活配置默认功能权限。 +2. **工作流文档可见性缺乏精细化受控与覆盖机制**: + - 目前受控工作流交付物对项目成员默安全域一视同仁,缺乏工作流基线可见性规则; + - 关键涉密文档(如涉及未公开核心光学算法、敏感供应链成本底牌)无法由文档负责人或主管单独配置“单文档可见性覆盖”(例如:收紧为仅三权责任人可见,或仅指定角色可见); + - 无法满足“单文档覆盖规则强于工作流默认规则”的严谨层级治理。 +3. **关键业务动作缺乏统一权限拦截控制**: + - 发起新研发项目(立项)、指派文档法定三权责任人、配置系统权限等高危操作缺乏严格的角色权限拦截与 UI 状态响应。 + +--- + +## 二、 三层正交解耦权限模型体系 (Three-Tier Orthogonal Model) + +为彻底解决上述矛盾,系统确立 **“全局动态角色 (Global RBAC) + 项目法定三权 (Project-Scoped Duties) + 单文档资源覆盖 (Doc-Level Visibility Override)”** 的三层正交解耦架构: + +``` +┌────────────────────────────────────────────────────────────────────────────────────────┐ +│ 第一层:全局角色与权限库 (Global RBAC) │ +│ • 实体:RoleDefinition (支持动态 CRUD 增删改查) │ +│ • 职能:用户全局绑定唯一角色,赋予默认功能权限集 (创建项目、管理权限、指派负责人等) │ +└───────────────────────────────────────────┬────────────────────────────────────────────┘ + │ 作用于项目与流程协作 +┌───────────────────────────────────────────▼────────────────────────────────────────────┐ +│ 第二层:项目法定三权分立 (Project-Scoped Duties) │ +│ • 实体:DocPermissionRole (编制人 Author / 审核人 Reviewer / 批准人 Approver) │ +│ • 职能:跟随具体项目与受控流转状态机(认领、就地编辑、送审、审核驳回、终审盖章锁定) │ +└───────────────────────────────────────────┬────────────────────────────────────────────┘ + │ 作用于交付物资源访问 +┌───────────────────────────────────────────▼────────────────────────────────────────────┐ +│ 第三层:交付物可见性继承与单文档覆盖 (Visibility & Override) │ +│ • 工作流默认策略 (Workflow Baseline): 阶段/工作流内文档的基线可见范围 (workflow:view_all) │ +│ • 单文档覆盖策略 (Doc-level Override): 独立策略覆盖工作流基线(收紧或放宽) │ +└────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、 数据模型与接口契约设计 (Data Contracts & Models) + +在 `web/src/types/rbac.ts` 中确立核心类型定义,并与 `web/src/types/docgov.ts`、`web/src/types/workbench.ts` 保持向下兼容与解耦。 + +### 1. 原子功能权限项 (`PermissionKey`) +```typescript +export type PermissionKey = + | 'project:create' // 创建新项目/立项推演 + | 'project:assign_lead' // 设置/变更文档责任人(编制/审核/批准) + | 'perm:manage' // 系统权限与角色管理配置 (进入管理中心增删改角色) + | 'workflow:config_visibility' // 配置工作流默认可见性基线 + | 'doc:override_visibility' // 配置单文档可见性覆盖策略 + | 'doc:export' // 导出受控交付物 (DOCX/XLSX) + | 'workflow:view_all'; // 默认查阅常规工作流文档 +``` + +### 2. 全局动态角色定义 (`RoleDefinition`) +```typescript +export interface RoleDefinition { + id: string; // 唯一标识:如 'role-admin', 'role-director', 'role-custom-qa' + name: string; // 显示名称:如 "系统管理员", "研发总监", "方案工程师" + code: string; // 唯一代码:如 "ADMIN", "DIRECTOR", "ENGINEER" + description: string; // 职责描述 + isSystem?: boolean; // 系统内置防死锁标记 (内置管理员角色不可删除) + permissions: PermissionKey[]; // 默认拥有的全局权限点列表 + createdAt: string; // 创建时间 + updatedAt: string; // 更新时间 +} +``` + +### 3. 用户全局属性扩展 (`UserProfile`) +```typescript +export interface UserProfile { + id: number; + name: string; + employeeId: string; + department: string; + position: string; // 岗位职称 (如 "资深医疗器械方案工程师") + roleId: string; // 关联的全局角色 ID (全局唯一) + customPermissions?: PermissionKey[]; // 用户个人级特例微调权限 (可选) + projectPermissions?: Record; // 项目内法定职责记录 (docId -> role) +} +``` + +### 4. 单文档可见性覆盖规则 (`DocVisibilityRule`) +受控交付物实体 `DeliverableItem` 扩展 `visibilityRule` 可选配置项: + +```typescript +export type VisibilityMode = + | 'inherit' // 默认:继承工作流/阶段默认可见性 + | 'role_whitelist' // 覆盖:仅允许特定全局角色可见 (如仅总监、质保QA) + | 'designated_only' // 覆盖:严格收紧,仅该文档当前的三权责任人可见 + | 'public'; // 覆盖:放宽,所有成员乃至访客均可查阅 + +export interface DocVisibilityRule { + mode: VisibilityMode; + allowedRoleIds?: string[]; // 当 mode === 'role_whitelist' 时生效 + allowedUserIds?: number[]; // 特例用户白名单 (例如外聘临床专家工号) + overrideReason?: string; // 覆盖原因说明 (质量合规留痕审计) + configuredBy?: string; // 设置人姓名 + configuredAt?: string; // 设置时间 +} + +export interface DeliverableItem { + // ...既有字段 (docId, projectId, title, stage, category, author, reviewer, approver 等) + visibilityRule?: DocVisibilityRule; // 单文档可见性覆盖规则 +} +``` + +--- + +## 四、 权限判定决策引擎设计 (Permission Engine & Pipeline) + +在 `web/src/engine/rbac.ts` 中实现高内聚、无副作用的纯函数判定引擎。 + +### 1. 全局功能权限判定 (`hasPermission`) +```typescript +export function hasPermission( + user: UserProfile, + roles: RoleDefinition[], + permKey: PermissionKey +): boolean { + const role = roles.find((r) => r.id === user.roleId); + if (!role) return false; + + // 超级管理员或具备权限管理特权者通配放行 + if (role.code === 'ADMIN' || role.permissions.includes('perm:manage')) { + return true; + } + + // 个人特例微调权限优先 + if (user.customPermissions?.includes(permKey)) { + return true; + } + + return role.permissions.includes(permKey); +} +``` + +### 2. 文档可见性 5 步决策流水线 (`isDeliverableVisible`) +严格遵循短路求值(Short-circuit Evaluation),解决工作流基线与单文档覆盖的层级冲突: + +``` +[用户请求访问交付物] + │ + ▼ +[步骤 1: 个人私密隔离检查] +文档 category === 'private'? ──(是)──> 用户是否为创建者 (creatorId === user.id)? ──(否)──> 🚫 隐藏/拒绝 + │ (否) │ (是) + │ ▼ + │ ✅ 允许查阅 + ▼ +[步骤 2: 管理员审计特权] +用户是否拥有 perm:manage 或 ADMIN 角色? ──(是)──> ✅ 允许查阅 + │ (否) + ▼ +[步骤 3: 法定三权责任人保底] +用户是否为该文档的编制人/审核人/批准人? ──(是)──> ✅ 允许查阅 (防业务断档) + │ (否) + ▼ +[步骤 4: 单文档覆盖策略判定 (Override Priority)] +文档是否存在 visibilityRule 且 mode !== 'inherit'? + ├─ 'designated_only' ──> 🚫 拒绝查阅 (步骤3已确认当前用户非责任人) + ├─ 'public' ──────────> ✅ 允许查阅 + ├─ 'role_whitelist' ──> 当前用户全局角色是否在 allowedRoleIds 中? ──(是)──> ✅ / (否)──> 🚫 + │ + ▼ (若 mode 为 'inherit' 或未显式配置) +[步骤 5: 工作流基线继承 (Workflow Baseline)] +回退继承工作流基线:用户角色是否具备 workflow:view_all 权限? ──(是)──> ✅ / (否)──> 🚫 +``` + +### 3. 业务动作权限判定 (`canPerformDocAction`) +- `project:create`:必须通过 `hasPermission(user, roles, 'project:create')` 校验; +- `project:assign_lead`:必须通过 `hasPermission(user, roles, 'project:assign_lead')` 校验; +- `doc:override_visibility`:必须通过 `hasPermission(user, roles, 'doc:override_visibility')` 校验。 + +--- + +## 五、 最小可测试框架与界面设计 (UI & Testing Touchpoints) + +为确保能够立即测试和实际应用,最小框架闭环落实以下四大触点: + +### 1. 动态角色管理控制台 (`SettingsModal` 升级) +- **入口**:点击左下角系统设置齿轮图标打开; +- **动态角色 CRUD**: + - 左侧展示系统所有角色列表(支持展示系统角色徽章 `SYSTEM` 与普通角色); + - “+ 新建角色”按钮,支持输入角色名称、代码和描述; + - 支持删除非系统内置角色(带有确认弹框); +- **权限配置矩阵**: + - 右侧呈现 7 项原子权限开关/复选框; + - 支持一键为当前选中的角色勾选/反选对应权限; +- **用户角色委派与模拟切换 (Quick Switch & Test)**: + - 支持将张工、李主管、王总监快速绑定到指定全局角色; + - 界面提供用户一键切换工具,立刻直观感受不同身份下的权限差异; +- **持久化**:角色库与用户角色绑定状态通过 `localStorage` 自动持久化存储。 + +### 2. 单文档可见性覆盖配置弹窗 (`DocVisibilityModal`) +- **入口**:在受控交付物详情头栏、大盘右侧或抽屉卡片上,点击【🔒 权限覆盖】图标; +- **权限守卫**:若当前用户无 `doc:override_visibility` 权限,该按钮置灰或隐藏; +- **覆盖模式选择器**: + - 单选:`继承工作流默认` / `仅三权责任人可见` / `指定角色白名单` / `全员公开`; + - 当选择“指定角色白名单”时,列出动态角色多选列表供勾选; + - 附带“覆盖原因”输入框(合规审计说明); +- **生效反馈**:保存后文档立即绑定 `visibilityRule`,交付物大盘与目录即刻重算可见性。 + +### 3. 核心业务动作受控联动拦截 (Realtime Interception) +- **创建项目受控**: + - 顶部导航栏的【+ 新建项目】按钮,无 `project:create` 权限时置灰禁用,鼠标悬停提示:“当前角色 [xxx] 无权发起新研发项目”; +- **设置文档负责人受控**: + - 用户项目权限弹窗与文档详情的三权责任人下拉框,无 `project:assign_lead` 权限时锁定只读; +- **文档可见性实时隐藏联动**: + - 当任一文档被配置为覆盖模式(如“仅研发总监可见”),切换登录人至“方案工程师”后,该文档在左侧目录树、交付物抽屉大盘中完全消失,不可点击访问。 + +--- + +## 六、 自动化测试套件与质量验证方案 (Test Plan & Verification) + +遵循仓库质量规范与 `test-reports-standard.mdc` 要求,测试分为单元测试矩阵与端到端实测验证: + +### 1. 自动化单元测试矩阵 (`web/src/engine/rbac.test.ts`) +- **用例 1 (角色 CRUD 与默认权限绑定)**:验证角色动态添加、编辑、权限分配与持久化序列化; +- **用例 2 (系统管理员特权)**:验证 ADMIN 角色及具备 `perm:manage` 权限者的全局通配放行; +- **用例 3 (工作流基线查阅权)**:验证具备 `workflow:view_all` 与无该权限的访客的基线过滤行为; +- **用例 4 (单文档覆盖 - designated_only)**:验证仅编制/审核/批准人可见,其他项目成员被精确拦截; +- **用例 5 (单文档覆盖 - role_whitelist)**:验证仅白名单内角色(如总监)可见,其他角色被精确拦截; +- **用例 6 (单文档覆盖 - public)**:验证无 `workflow:view_all` 权限的普通访客在公开单文档上成功查阅; +- **用例 7 (私密文档隔离强校验)**:验证即使具有超级权限或角色白名单,非创建者的 private 文档绝对不可见; +- **用例 8 (核心动作权限拦截)**:验证 `project:create` 与 `project:assign_lead` 的判定契约。 + +### 2. 测试报告归档 +所有测试结果必须归档于: +`.doc/test-reports/2026-09-08-dynamic-roles-and-doc-visibility-rbac-test-report.md` + +--- + +## 七、 实施与渐进式提交计划 (Progressive Commits) + +严格遵循仓库 Conventional Commits 与渐进式提交铁律: +1. `feat(rbac)`: 增加 RBAC 数据模型契约与权限判定引擎纯函数 (`rbac.ts`, `rbac.test.ts`) +2. `feat(web)`: 升级 `SettingsModal` 实现动态角色增删改与权限配置面板 +3. `feat(web)`: 新增 `DocVisibilityModal` 实现单文档可见性覆盖配置与大盘联动 +4. `feat(web)`: 联动「新建项目」与「指派负责人」动作权限拦截控制 +5. `test(rbac)`: 运行全量测试套件并归档自动化测试报告 +6. `docs(spec)`: 归档本设计规范文档并同步关联引用 From b52f72c6fc3d9d1dd1c074811b225ef227546a75 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:29:36 +0800 Subject: [PATCH 02/18] docs(plan): add dynamic roles and doc visibility rbac implementation plan Co-authored-by: Cursor --- ...8-dynamic-roles-and-doc-visibility-rbac.md | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-08-dynamic-roles-and-doc-visibility-rbac.md diff --git a/docs/superpowers/plans/2026-09-08-dynamic-roles-and-doc-visibility-rbac.md b/docs/superpowers/plans/2026-09-08-dynamic-roles-and-doc-visibility-rbac.md new file mode 100644 index 00000000..6249d38c --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-dynamic-roles-and-doc-visibility-rbac.md @@ -0,0 +1,310 @@ +# 动态角色管理与单文档可见性覆盖权限系统实现计划 +# (Dynamic Roles & Document Visibility Override RBAC Implementation Plan) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 实现医疗器械全流程智能协同研发系统中的全局动态角色管理 (RBAC)、工作流文档可见性基线与单文档可见性覆盖 (Override) 机制,并完成关键业务动作(新建项目、指派文档负责人、文档覆盖配置)的权限拦截与交互式测试验证。 + +**Architecture:** 采用“全局动态角色 (RBAC) + 项目法定三权 (Project-Scoped Duties) + 单文档资源覆盖 (Doc-Level Visibility Override)”三层解耦模型。通过纯函数决策引擎 `isDeliverableVisible` 与 `hasPermission` 统一计算访问权,结合前端响应式 UI(动态角色管理面板、单文档可见性覆盖弹窗、动作拦截置灰与自动隐藏)与 localStorage 本地持久化,构建最小可测试与应用的完整闭环。 + +**Tech Stack:** React 19 + TypeScript 5.9 + Vite 7 + FlowMind Token 设计系统 + Node.js 内置 Test Runner (`tsx --test`) + Vitest + +## Global Constraints + +- 严禁把所有改动揉合成单次庞大提交,严格执行分层渐进式提交:`feat(rbac)`, `feat(web)`, `test(rbac)`, `docs(test)`. +- 纯函数逻辑优先:权限判定流水线与单文档覆盖必须在纯函数中完成计算并实现 100% 单元测试覆盖,严禁副作用侵入基础组件. +- 三层交付物隔离红线:`private` 文档除创建者本人外绝对禁止任何人查阅(即使是 ADMIN 或单文档覆盖也不能穿透隐私隔离). +- 文档三权流转纯净性:受控工作流交付物(`workflow`)必须保持严格法定三权流转,单文档覆盖仅影响“可否查阅”,不可篡改生命周期状态机. +- 测试报告归档必须存放于 `.doc/test-reports/` 规范目录下. + +--- + +### Task 1: 定义 RBAC 数据契约与交付物/用户扩展类型 (Data Contracts & Types) + +**Files:** +- Create: `web/src/types/rbac.ts` +- Modify: `web/src/types/docgov.ts:20-33` +- Modify: `web/src/types/workbench.ts:95-117` +- Test: `web/src/engine/rbac.test.ts` (将在 Task 2 建立完整测试) + +**Interfaces:** +- Produces: + - `PermissionKey`: `'project:create' | 'project:assign_lead' | 'perm:manage' | 'workflow:config_visibility' | 'doc:override_visibility' | 'doc:export' | 'workflow:view_all'` + - `RoleDefinition`: `{ id: string; name: string; code: string; description: string; isSystem?: boolean; permissions: PermissionKey[]; createdAt: string; updatedAt: string; }` + - `VisibilityMode`: `'inherit' | 'role_whitelist' | 'designated_only' | 'public'` + - `DocVisibilityRule`: `{ mode: VisibilityMode; allowedRoleIds?: string[]; allowedUserIds?: number[]; overrideReason?: string; configuredBy?: string; configuredAt?: string; }` + - `DeliverableItem.visibilityRule?: DocVisibilityRule` + - `UserProfile.roleId: string` + +- [ ] **Step 1: 创建 `web/src/types/rbac.ts` 类型契约文件** + +```typescript +import { DocPermissionRole } from './docgov'; + +export type PermissionKey = + | 'project:create' // 创建新项目/立项 + | 'project:assign_lead' // 设置/变更文档责任人(编制/审核/批准) + | 'perm:manage' // 系统权限与角色管理配置 + | 'workflow:config_visibility' // 配置工作流默认可见性 + | 'doc:override_visibility' // 配置单文档可见性覆盖 + | 'doc:export' // 导出受控交付物(DOCX/XLSX) + | 'workflow:view_all'; // 默认查阅常规工作流文档 + +export interface RoleDefinition { + id: string; // 唯一标识:如 'role-admin', 'role-director', 'role-engineer' + name: string; // 显示名称:如 "系统管理员", "研发总监", "方案工程师" + code: string; // 唯一代码:如 "ADMIN", "DIRECTOR", "ENGINEER" + description: string; // 职责描述 + isSystem?: boolean; // 系统内置防死锁标记 (内置角色不可被删除) + permissions: PermissionKey[]; // 默认拥有的全局权限集 + createdAt: string; + updatedAt: string; +} + +export type VisibilityMode = + | 'inherit' // 默认:继承工作流/阶段默认可见性 + | 'role_whitelist' // 覆盖:仅指定全局角色可见 (如仅总监、质保QA) + | 'designated_only' // 覆盖:严格收紧,仅该文档当前的三权责任人可见 + | 'public'; // 覆盖:放宽,所有成员均可查阅 + +export interface DocVisibilityRule { + mode: VisibilityMode; + allowedRoleIds?: string[]; // 当 mode === 'role_whitelist' 时生效 + allowedUserIds?: number[]; // 特例用户白名单 (例如特聘临床专家) + overrideReason?: string; // 覆盖原因说明 (合规留痕审计) + configuredBy?: string; // 设置人姓名 + configuredAt?: string; // 设置时间 +} +``` + +- [ ] **Step 2: 扩展 `web/src/types/docgov.ts` 中的 `UserProfile`** + +为 `UserProfile` 添加 `roleId: string` 与可选 `customPermissions?: PermissionKey[]`。 + +- [ ] **Step 3: 扩展 `web/src/types/workbench.ts` 中的 `DeliverableItem`** + +为 `DeliverableItem` 增加可选字段 `visibilityRule?: DocVisibilityRule`。 + +- [ ] **Step 4: 运行 `npm test` 验证类型定义向下兼容性** + +Run: `npm test` +Expected: 40 tests passed, 0 failures. + +- [ ] **Step 5: 提交数据契约** + +```bash +git add web/src/types/rbac.ts web/src/types/docgov.ts web/src/types/workbench.ts +git commit -m "feat(rbac): define rbac data contracts and extend deliverable and user types" +``` + +--- + +### Task 2: 实现权限判定纯函数引擎与全量单元测试 (Permission Engine & Unit Tests) + +**Files:** +- Create: `web/src/engine/rbac.ts` +- Create: `web/src/engine/rbac.test.ts` + +**Interfaces:** +- Produces: + - `DEFAULT_ROLES: RoleDefinition[]` + - `hasPermission(user: UserProfile, roles: RoleDefinition[], permKey: PermissionKey): boolean` + - `isDeliverableVisible(user: UserProfile, roles: RoleDefinition[], doc: DeliverableItem): boolean` + - `filterVisibleDeliverablesWithRBAC(deliverables: DeliverableItem[], user: UserProfile, roles: RoleDefinition[]): DeliverableItem[]` + - `canPerformDocAction(action: 'assign_lead' | 'override_visibility' | 'create_project', user: UserProfile, roles: RoleDefinition[]): boolean` + +- [ ] **Step 1: 编写单元测试 `web/src/engine/rbac.test.ts` (覆盖 8 大核心场景)** + +测试清单: +1. 默认系统角色与自定义角色权限解析 +2. 超级管理员 (ADMIN) 与具备 `perm:manage` 用户的通配豁免特权 +3. 工作流基线可见性 (`workflow:view_all`) 判定 +4. 单文档覆盖:`designated_only`(仅编制/审核/批准人可见,其他用户被拦截) +5. 单文档覆盖:`role_whitelist`(仅指定角色可见,白名单外角色被拦截) +6. 单文档覆盖:`public`(放宽可见性,无 `workflow:view_all` 权限的普通访客也能查阅) +7. 私密文档隔离强红线:`category === 'private'` 无论任何管理员或单文档覆盖,非创建者本人绝对不可见 +8. 核心业务动作权限拦截:`project:create` 与 `project:assign_lead` + +- [ ] **Step 2: 运行测试验证失败 (TDD 红灯)** + +Run: `npx tsx --test web/src/engine/rbac.test.ts` +Expected: FAIL with "Cannot find module '../engine/rbac'". + +- [ ] **Step 3: 编写 `web/src/engine/rbac.ts` 纯函数引擎** + +实现 `DEFAULT_ROLES` 初始预置角色(管理员、研发总监、主管、方案工程师、外聘访客),实现 `hasPermission`、`isDeliverableVisible` 与 5 步决策流水线。 + +- [ ] **Step 4: 运行测试验证通过 (TDD 绿灯)** + +Run: `npx tsx --test web/src/engine/rbac.test.ts` +Expected: PASS all 8 test cases. + +- [ ] **Step 5: 提交权限引擎** + +```bash +git add web/src/engine/rbac.ts web/src/engine/rbac.test.ts +git commit -m "feat(rbac): implement permission evaluation engine and unit test suite" +``` + +--- + +### Task 3: 升级系统设置面板实现动态角色 CRUD 与权限配置 (SettingsModal UI) + +**Files:** +- Modify: `web/src/components/user/SettingsModal.tsx` +- Test: 手动与集成测试验证 + +**Interfaces:** +- Consumes: + - `RoleDefinition`, `PermissionKey` from `web/src/types/rbac` + - `DEFAULT_ROLES` from `web/src/engine/rbac` +- Produces: + - `SettingsModalProps` 增加 `roles: RoleDefinition[]`, `onSaveRoles: (roles: RoleDefinition[]) => void`, `users: UserProfile[]`, `currentUser: UserProfile`, `onUpdateUserRole: (userId: number, roleId: string) => void`, `onSwitchUser: (user: UserProfile) => void` + +- [ ] **Step 1: 扩展 `SettingsModalProps` 接口** + +增加角色与用户管理相关的回调属性与状态绑定。 + +- [ ] **Step 2: 在 `SettingsModal.tsx` 中实现双栏管理布局** + +- 左栏:角色列表(展示内置角色徽章、自定义角色,底部提供「+ 新增自定义角色」按钮与删除按钮); +- 右栏: + - 角色基本信息(名称、代码、描述); + - 7 大原子权限项复选矩阵(点击实时勾选/解绑,系统角色 ADMIN 默认只读全选); + - 用户角色委派与当前登录人快速切换区(将张工、李主管、王总监快速分配给不同角色,一键切换测试)。 + +- [ ] **Step 3: 增加 localStorage 持久化支持** + +在 `web/src/data/storage.ts` 或组件状态中支持 `loadRoles()` 与 `saveRoles()`,确保刷新浏览器后新增角色与权限配置不丢失。 + +- [ ] **Step 4: 运行前端构建与既有测试检验语法** + +Run: `npm test` +Expected: All tests pass. + +- [ ] **Step 5: 提交系统设置动态角色面板** + +```bash +git add web/src/components/user/SettingsModal.tsx web/src/data/storage.ts +git commit -m "feat(web): upgrade settings modal with dynamic roles CRUD and permission matrix" +``` + +--- + +### Task 4: 实现单文档可见性覆盖弹窗与大盘徽章联动 (DocVisibilityModal UI) + +**Files:** +- Create: `web/src/components/doc/DocVisibilityModal.tsx` +- Modify: `web/src/components/doc/DocGovActionBar.tsx:270-340` +- Modify: `web/src/components/doc/DeckDrawer.tsx:390-430` + +**Interfaces:** +- Consumes: + - `DocVisibilityRule`, `VisibilityMode`, `RoleDefinition` from `web/src/types/rbac` + - `DeliverableItem` from `web/src/types/workbench` + - `hasPermission` from `web/src/engine/rbac` +- Produces: + - ` ...} />` + - `DocGovActionBar` 中的「🔒 可见性覆盖」按钮与状态提示 + - `DeckDrawer` 卡片中的「🔒 覆盖受控」彩色角标 + +- [ ] **Step 1: 编写 `web/src/components/doc/DocVisibilityModal.tsx`** + +包含 4 种覆盖模式单选、白名单角色勾选列表、覆盖原因输入框、保存与清除覆盖规则按钮。 + +- [ ] **Step 2: 在 `DocGovActionBar.tsx` 中增加权限覆盖操作入口** + +根据当前用户是否具备 `doc:override_visibility` 权限(或管理员),在受控工作流交付物操作栏呈现【🔒 可见性覆盖】按钮。若当前文档已设置覆盖规则,展示明显的模式标签(如“仅三权责任人可见”)。 + +- [ ] **Step 3: 在 `DeckDrawer.tsx` 交付物大盘卡片增加覆盖标记** + +若卡片文档存在 `visibilityRule && visibilityRule.mode !== 'inherit'`,渲染醒目的黄色/紫色安全微标:`🔒 仅责任人可见` 或 `🔒 角色白名单`。 + +- [ ] **Step 4: 运行测试确保无渲染与类型破坏** + +Run: `npm test` +Expected: All tests pass. + +- [ ] **Step 5: 提交单文档可见性覆盖组件与卡片徽章** + +```bash +git add web/src/components/doc/DocVisibilityModal.tsx web/src/components/doc/DocGovActionBar.tsx web/src/components/doc/DeckDrawer.tsx +git commit -m "feat(web): add doc visibility override modal and badges in deck drawer" +``` + +--- + +### Task 5: 全局集成、状态持久化与关键受控动作拦截 (Global Integration & Interception) + +**Files:** +- Modify: `web/src/App.tsx` +- Modify: `web/src/components/layout/LeftRail.tsx:65-90` +- Modify: `web/src/components/user/UserProjectRightsModal.tsx:450-480` +- Test: `web/src/engine/rbacIntegration.test.ts` + +**Interfaces:** +- Consumes: + - `isDeliverableVisible`, `hasPermission`, `filterVisibleDeliverablesWithRBAC` + - `DocVisibilityModal`, `SettingsModal` +- Produces: + - 核心动作受控拦截:「新建立项会话」无 `project:create` 权限时禁用并提示; + - 核心动作受控拦截:「指派文档责任人」无 `project:assign_lead` 权限时选择器锁定只读; + - 实时可见性过滤:配置单文档覆盖后,大盘、左侧目录与当前激活文档实时响应隐藏与自动跳选。 + +- [ ] **Step 1: 在 `web/src/App.tsx` 中初始化角色状态与可见性计算** + +使用 `loadRoles()` 初始化 `roles` 状态;将 `visibleDeliverables` 过滤逻辑升级为调用 `filterVisibleDeliverablesWithRBAC(currentProjectDeliverables, currentUser, roles)`。 + +- [ ] **Step 2: 在 `LeftRail.tsx` 拦截「新建立项会话」** + +判定 `hasPermission(currentUser, roles, 'project:create')`。若无权限,按钮禁用、透明度降低,悬停提示:“当前角色 [xxx] 无权发起新研发项目”。 + +- [ ] **Step 3: 在 `UserProjectRightsModal.tsx` 拦截「指派文档负责人」** + +判定 `hasPermission(currentUser, roles, 'project:assign_lead')`。若无权限,责任人下拉选择器置为 `disabled`,悬停提示:“当前角色无权配置文档法定职责”。 + +- [ ] **Step 4: 编写全链路集成测试 `web/src/engine/rbacIntegration.test.ts`** + +覆盖全流程联动测试: +1. 动态新增自定义角色 "外聘合规审计员" 并赋予指定权限; +2. 为某份工作流文档设置“仅三权责任人可见”单文档覆盖; +3. 切换登录用户为不同全局角色,断言可见列表、创建项目权限与指派负责人权限均严格受控。 + +- [ ] **Step 5: 运行全量测试** + +Run: `npm test` +Expected: All tests pass. + +- [ ] **Step 6: 提交全局集成与受控拦截** + +```bash +git add web/src/App.tsx web/src/components/layout/LeftRail.tsx web/src/components/user/UserProjectRightsModal.tsx web/src/engine/rbacIntegration.test.ts +git commit -m "feat(web): wire rbac permission enforcement into left rail and rights modal" +``` + +--- + +### Task 6: 执行全量自动化回归与归档测试报告 (Test Suite & Report Archival) + +**Files:** +- Create: `.doc/test-reports/2026-09-08-dynamic-roles-and-doc-visibility-rbac-test-report.md` + +- [ ] **Step 1: 运行全量测试并捕获控制台输出** + +Run: `npm test` +Expected: 100% pass across all test files. + +- [ ] **Step 2: 撰写标准化测试报告** + +严格遵循 `test-reports-standard.mdc` 模板结构,录入: +- 背景与目标 +- 测试方案与环境 +- 8 大单元用例 + 3 大全链路集成用例真实输入/输出/判定 +- 终端运行耗时统计与测试结论 + +- [ ] **Step 3: 提交测试报告与更新文档** + +```bash +git add .doc/test-reports/2026-09-08-dynamic-roles-and-doc-visibility-rbac-test-report.md +git commit -m "test(rbac): archive dynamic roles and doc visibility rbac test report" +``` From aa9636c10f0493d2ea4176e80b305f877e487487 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:30:39 +0800 Subject: [PATCH 03/18] feat(rbac): define rbac data contracts and extend deliverable and user types Co-authored-by: Cursor --- web/src/types/docgov.ts | 4 ++++ web/src/types/rbac.ts | 34 ++++++++++++++++++++++++++++++++++ web/src/types/workbench.ts | 2 ++ 3 files changed, 40 insertions(+) create mode 100644 web/src/types/rbac.ts diff --git a/web/src/types/docgov.ts b/web/src/types/docgov.ts index fe9d81a1..1d7b2a6c 100644 --- a/web/src/types/docgov.ts +++ b/web/src/types/docgov.ts @@ -1,3 +1,5 @@ +import { PermissionKey } from './rbac'; + export type DocLifecycleState = | 'template' | 'drafting' @@ -25,6 +27,8 @@ export interface UserProfile { department: string; position: string; // 固定职位,不随项目改变 avatar?: string; + roleId?: string; // 关联的全局唯一角色ID (如 'role-admin', 'role-director', 'role-engineer') + customPermissions?: PermissionKey[]; // 用户个人级特例微调权限 role?: UserRole; // 兼容旧接口 title?: string; // 兼容旧接口 projectPermissions?: Record; // docId -> role diff --git a/web/src/types/rbac.ts b/web/src/types/rbac.ts new file mode 100644 index 00000000..c0d5e87a --- /dev/null +++ b/web/src/types/rbac.ts @@ -0,0 +1,34 @@ +export type PermissionKey = + | 'project:create' // 创建新项目/立项 + | 'project:assign_lead' // 设置/变更文档责任人(编制/审核/批准) + | 'perm:manage' // 系统权限与角色管理配置 (进入管理中心增删改角色) + | 'workflow:config_visibility' // 配置工作流默认可见性 + | 'doc:override_visibility' // 配置单文档可见性覆盖 + | 'doc:export' // 导出受控交付物(DOCX/XLSX) + | 'workflow:view_all'; // 默认查阅常规工作流文档 + +export interface RoleDefinition { + id: string; // 唯一标识:如 'role-admin', 'role-director', 'role-engineer' + name: string; // 显示名称:如 "系统管理员", "研发总监", "方案工程师" + code: string; // 唯一代码:如 "ADMIN", "DIRECTOR", "ENGINEER" + description: string; // 职责描述 + isSystem?: boolean; // 系统内置防死锁标记 (内置角色不可被删除) + permissions: PermissionKey[]; // 默认拥有的全局权限集 + createdAt: string; + updatedAt: string; +} + +export type VisibilityMode = + | 'inherit' // 默认:继承工作流/阶段默认可见性 + | 'role_whitelist' // 覆盖:仅指定全局角色可见 (如仅总监、质保QA) + | 'designated_only' // 覆盖:严格收紧,仅该文档当前的三权责任人可见 + | 'public'; // 覆盖:放宽,所有成员均可查阅 + +export interface DocVisibilityRule { + mode: VisibilityMode; + allowedRoleIds?: string[]; // 当 mode === 'role_whitelist' 时生效 + allowedUserIds?: number[]; // 特例用户白名单 (例如特聘临床专家) + overrideReason?: string; // 覆盖原因说明 (合规留痕审计) + configuredBy?: string; // 设置人姓名 + configuredAt?: string; // 设置时间 +} diff --git a/web/src/types/workbench.ts b/web/src/types/workbench.ts index 6f201491..bd133372 100644 --- a/web/src/types/workbench.ts +++ b/web/src/types/workbench.ts @@ -1,4 +1,5 @@ import { DocLifecycleState, UserRole } from './docgov'; +import { DocVisibilityRule } from './rbac'; export type StageKey = 'V0.1' | 'V0.5' | 'V1.0'; @@ -113,6 +114,7 @@ export interface DeliverableItem { updatedAt: string; summary?: string; sourceMessageSnippet?: string; + visibilityRule?: DocVisibilityRule; } export interface WorkbenchState { From bd52108c8afce97b264a37648a9b45b001173d09 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:31:50 +0800 Subject: [PATCH 04/18] feat(rbac): implement permission evaluation engine and unit test suite Co-authored-by: Cursor --- web/src/engine/rbac.test.ts | 253 ++++++++++++++++++++++++++++++++++++ web/src/engine/rbac.ts | 215 ++++++++++++++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 web/src/engine/rbac.test.ts create mode 100644 web/src/engine/rbac.ts diff --git a/web/src/engine/rbac.test.ts b/web/src/engine/rbac.test.ts new file mode 100644 index 00000000..1d940db9 --- /dev/null +++ b/web/src/engine/rbac.test.ts @@ -0,0 +1,253 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + DEFAULT_ROLES, + hasPermission, + isDeliverableVisible, + filterVisibleDeliverablesWithRBAC, + canPerformDocAction, +} from './rbac'; +import { UserProfile } from '../types/docgov'; +import { DeliverableItem } from '../types/workbench'; +import { RoleDefinition } from '../types/rbac'; + +const sampleUsers: Record = { + admin: { + id: 100, + name: '系统管理员', + employeeId: 'ADMIN-001', + department: '系统运维部', + position: '系统管理员', + roleId: 'role-admin', + }, + director: { + id: 3, + name: '王总监', + employeeId: 'SMDF-0018', + department: '医疗器械研发事业部', + position: '微创器械研发总监', + roleId: 'role-director', + }, + lead: { + id: 2, + name: '李主管', + employeeId: 'SMDF-0205', + department: '机器人与微创内窥镜研发中心', + position: '研发项目技术主管', + roleId: 'role-tech-lead', + }, + engineer: { + id: 1, + name: '张工', + employeeId: 'SMDF-0821', + department: '机器人与微创内窥镜研发中心', + position: '资深医疗器械方案工程师', + roleId: 'role-engineer', + }, + guest: { + id: 99, + name: '外部审计专员', + employeeId: 'GUEST-001', + department: '第三方认证机构', + position: '外部合规审查员', + roleId: 'role-guest', + }, +}; + +const createMockDoc = (overrides: Partial = {}): DeliverableItem => ({ + docId: 'doc-mock-001', + projectId: 1, + taskKey: 'feasibility', + title: '测试受控交付物', + stage: 'V0.1', + category: 'workflow', + creatorId: 1, + creatorName: '张工', + isFixedWorkflow: true, + isTemplateOnly: false, + lifecycleState: 'drafting', + isClaimed: true, + author: '张工', + reviewer: '李主管', + approver: '王总监', + updatedAt: '2026-09-08 12:00', + ...overrides, +}); + +test('用例 1: DEFAULT_ROLES 包含 5 种默认角色且具备预期权限', () => { + assert.equal(DEFAULT_ROLES.length, 5); + const admin = DEFAULT_ROLES.find((r) => r.code === 'ADMIN'); + const engineer = DEFAULT_ROLES.find((r) => r.code === 'ENGINEER'); + const guest = DEFAULT_ROLES.find((r) => r.code === 'GUEST'); + + assert.ok(admin); + assert.ok(admin.isSystem); + assert.ok(admin.permissions.includes('perm:manage')); + + assert.ok(engineer); + assert.ok(engineer.permissions.includes('workflow:view_all')); + assert.equal(engineer.permissions.includes('project:create'), false); + + assert.ok(guest); + assert.equal(guest.permissions.includes('workflow:view_all'), false); +}); + +test('用例 2: 系统管理员 (ADMIN) 与具备 perm:manage 用户的通配豁免特权', () => { + assert.equal(hasPermission(sampleUsers.admin, DEFAULT_ROLES, 'project:create'), true); + assert.equal(hasPermission(sampleUsers.admin, DEFAULT_ROLES, 'perm:manage'), true); + assert.equal(hasPermission(sampleUsers.admin, DEFAULT_ROLES, 'doc:override_visibility'), true); + + // 普通工程师无管理权限 + assert.equal(hasPermission(sampleUsers.engineer, DEFAULT_ROLES, 'perm:manage'), false); + assert.equal(hasPermission(sampleUsers.engineer, DEFAULT_ROLES, 'project:create'), false); +}); + +test('用例 3: 常规工作流基线可见性 (workflow:view_all) 判定', () => { + const normalDoc = createMockDoc(); + + // 工程师有 workflow:view_all -> 可见 + assert.equal(isDeliverableVisible(sampleUsers.engineer, DEFAULT_ROLES, normalDoc), true); + + // 外部审计员无 workflow:view_all 且不是责任人 -> 不可见 + assert.equal(isDeliverableVisible(sampleUsers.guest, DEFAULT_ROLES, normalDoc), false); +}); + +test('用例 4: 单文档覆盖 - designated_only (仅编制/审核/批准人可见,其他用户被精确拦截)', () => { + const restrictedDoc = createMockDoc({ + author: '张工', + reviewer: '李主管', + approver: '赵院长', // 王总监不是责任人 + visibilityRule: { + mode: 'designated_only', + overrideReason: '敏感临床事故报告,收紧仅责任人可见', + configuredBy: '李主管', + configuredAt: '2026-09-08 14:00', + }, + }); + + // 张工是编制人 -> 可见 + assert.equal(isDeliverableVisible(sampleUsers.engineer, DEFAULT_ROLES, restrictedDoc), true); + + // 李主管是审核人 -> 可见 + assert.equal(isDeliverableVisible(sampleUsers.lead, DEFAULT_ROLES, restrictedDoc), true); + + // 王总监虽为研发总监,但非三权责任人 -> 被单文档覆盖拦截 (不可见) + assert.equal(isDeliverableVisible(sampleUsers.director, DEFAULT_ROLES, restrictedDoc), false); + + // 系统管理员超级权限仍可见 + assert.equal(isDeliverableVisible(sampleUsers.admin, DEFAULT_ROLES, restrictedDoc), true); +}); + +test('用例 5: 单文档覆盖 - role_whitelist (仅白名单内角色可见,其他角色被精确拦截)', () => { + const secretPatentDoc = createMockDoc({ + author: '外聘专家', + reviewer: '外聘审查', + approver: '外聘顾问', + visibilityRule: { + mode: 'role_whitelist', + allowedRoleIds: ['role-director'], // 仅总监可见 + overrideReason: '核心专利保密算法拆解', + }, + }); + + // 王总监拥有 role-director -> 可见 + assert.equal(isDeliverableVisible(sampleUsers.director, DEFAULT_ROLES, secretPatentDoc), true); + + // 张工 (role-engineer) 不在白名单中 -> 不可见 + assert.equal(isDeliverableVisible(sampleUsers.engineer, DEFAULT_ROLES, secretPatentDoc), false); + + // 李主管 (role-tech-lead) 不在白名单中 -> 不可见 + assert.equal(isDeliverableVisible(sampleUsers.lead, DEFAULT_ROLES, secretPatentDoc), false); +}); + +test('用例 6: 单文档覆盖 - public (放宽可见性,无 workflow:view_all 权限的访客也能查阅)', () => { + const publicSpecDoc = createMockDoc({ + author: '张工', + reviewer: '李主管', + approver: '王总监', + visibilityRule: { + mode: 'public', + overrideReason: '产品公开白皮书,供外部审计或用户公开查阅', + }, + }); + + // 访客原本无 workflow:view_all,但单文档设为 public -> 可见 + assert.equal(isDeliverableVisible(sampleUsers.guest, DEFAULT_ROLES, publicSpecDoc), true); +}); + +test('用例 7: 私密文档隔离强红线 (category === private 无论任何管理员或单文档覆盖,非创建者绝对不可见)', () => { + const privateDraft = createMockDoc({ + category: 'private', + creatorId: 1, // 张工创建 + creatorName: '张工', + visibilityRule: { + mode: 'public', // 即使误设 public 也绝不能穿透 private 隔离 + }, + }); + + // 创建者本人可见 + assert.equal(isDeliverableVisible(sampleUsers.engineer, DEFAULT_ROLES, privateDraft), true); + + // 其他人不可见 + assert.equal(isDeliverableVisible(sampleUsers.lead, DEFAULT_ROLES, privateDraft), false); + assert.equal(isDeliverableVisible(sampleUsers.director, DEFAULT_ROLES, privateDraft), false); + assert.equal(isDeliverableVisible(sampleUsers.admin, DEFAULT_ROLES, privateDraft), false); + assert.equal(isDeliverableVisible(sampleUsers.guest, DEFAULT_ROLES, privateDraft), false); +}); + +test('用例 8: 核心业务动作权限拦截判定 (canPerformDocAction & hasPermission)', () => { + // 1. 创建项目权限 + assert.equal(canPerformDocAction('create_project', sampleUsers.director, DEFAULT_ROLES), true); + assert.equal(canPerformDocAction('create_project', sampleUsers.lead, DEFAULT_ROLES), true); + assert.equal(canPerformDocAction('create_project', sampleUsers.engineer, DEFAULT_ROLES), false); + assert.equal(canPerformDocAction('create_project', sampleUsers.guest, DEFAULT_ROLES), false); + + // 2. 指派文档法定三权责任人 + assert.equal(canPerformDocAction('assign_lead', sampleUsers.director, DEFAULT_ROLES), true); + assert.equal(canPerformDocAction('assign_lead', sampleUsers.lead, DEFAULT_ROLES), true); + assert.equal(canPerformDocAction('assign_lead', sampleUsers.engineer, DEFAULT_ROLES), false); + + // 3. 配置单文档覆盖 + assert.equal(canPerformDocAction('override_visibility', sampleUsers.director, DEFAULT_ROLES), true); + assert.equal(canPerformDocAction('override_visibility', sampleUsers.lead, DEFAULT_ROLES), true); + assert.equal(canPerformDocAction('override_visibility', sampleUsers.engineer, DEFAULT_ROLES), false); + + // 4. 用户个人级特例微调 (customPermissions) 赋予方案工程师特例权限 + const customEngineer: UserProfile = { + ...sampleUsers.engineer, + customPermissions: ['project:create'], + }; + assert.equal(canPerformDocAction('create_project', customEngineer, DEFAULT_ROLES), true); +}); + +test('用例 9: filterVisibleDeliverablesWithRBAC 能够批处理过滤大盘交付物', () => { + const docs: DeliverableItem[] = [ + createMockDoc({ docId: 'doc-1', title: '常规工作流' }), + createMockDoc({ + docId: 'doc-2', + title: '绝密算法', + author: '外聘专家', + visibilityRule: { mode: 'role_whitelist', allowedRoleIds: ['role-director'] }, + }), + createMockDoc({ + docId: 'doc-3', + title: '张工个人草案', + category: 'private', + creatorId: 1, + }), + ]; + + // 张工视角: 能看常规工作流(doc-1)和个人草案(doc-3),不能看绝密算法(doc-2) + const zhangVisible = filterVisibleDeliverablesWithRBAC(docs, sampleUsers.engineer, DEFAULT_ROLES); + assert.deepEqual( + zhangVisible.map((d) => d.docId), + ['doc-1', 'doc-3'] + ); + + // 王总监视角: 能看常规工作流(doc-1)和绝密算法(doc-2),不能看张工私密草案(doc-3) + const wangVisible = filterVisibleDeliverablesWithRBAC(docs, sampleUsers.director, DEFAULT_ROLES); + assert.deepEqual( + wangVisible.map((d) => d.docId), + ['doc-1', 'doc-2'] + ); +}); diff --git a/web/src/engine/rbac.ts b/web/src/engine/rbac.ts new file mode 100644 index 00000000..311d1481 --- /dev/null +++ b/web/src/engine/rbac.ts @@ -0,0 +1,215 @@ +import { RoleDefinition, PermissionKey } from '../types/rbac'; +import { UserProfile } from '../types/docgov'; +import { DeliverableItem } from '../types/workbench'; + +/** + * 系统内置与预置全局角色库 + */ +export const DEFAULT_ROLES: RoleDefinition[] = [ + { + id: 'role-admin', + name: '系统管理员', + code: 'ADMIN', + description: '拥有系统所有功能与数据的全量管理权限(防死锁兜底)。', + isSystem: true, + permissions: [ + 'project:create', + 'project:assign_lead', + 'perm:manage', + 'workflow:config_visibility', + 'doc:override_visibility', + 'doc:export', + 'workflow:view_all', + ], + createdAt: '2026-09-08 00:00', + updatedAt: '2026-09-08 00:00', + }, + { + id: 'role-director', + name: '研发总监', + code: 'DIRECTOR', + description: '负责研发中心整体项目决策、立项审查、权限监督与终审批准。', + isSystem: false, + permissions: [ + 'project:create', + 'project:assign_lead', + 'workflow:config_visibility', + 'doc:override_visibility', + 'doc:export', + 'workflow:view_all', + ], + createdAt: '2026-09-08 00:00', + updatedAt: '2026-09-08 00:00', + }, + { + id: 'role-tech-lead', + name: '研发技术主管', + code: 'TECH_LEAD', + description: '负责具体研发管线的进度把控、文档责任人分配与技术审核。', + isSystem: false, + permissions: [ + 'project:create', + 'project:assign_lead', + 'doc:override_visibility', + 'doc:export', + 'workflow:view_all', + ], + createdAt: '2026-09-08 00:00', + updatedAt: '2026-09-08 00:00', + }, + { + id: 'role-engineer', + name: '方案工程师', + code: 'ENGINEER', + description: '负责医疗器械规格设计、受控正文编制、技术测算与推演。', + isSystem: false, + permissions: ['doc:export', 'workflow:view_all'], + createdAt: '2026-09-08 00:00', + updatedAt: '2026-09-08 00:00', + }, + { + id: 'role-guest', + name: '外部审计/访客', + code: 'GUEST', + description: '仅允许通过单文档覆盖或公开授权查阅特定报告,默认不可见内部工作流。', + isSystem: false, + permissions: ['doc:export'], + createdAt: '2026-09-08 00:00', + updatedAt: '2026-09-08 00:00', + }, +]; + +/** + * 判定用户是否具备指定全局功能权限 + */ +export function hasPermission( + user: UserProfile, + roles: RoleDefinition[], + permKey: PermissionKey +): boolean { + // 1. 查找用户角色定义 + const role = roles.find((r) => r.id === user.roleId); + + // 2. 超级管理员拥有全量特权通配 + if (role?.code === 'ADMIN' || role?.permissions.includes('perm:manage')) { + return true; + } + + // 3. 用户个人级特例微调优先 (追加权限) + if (user.customPermissions?.includes(permKey)) { + return true; + } + + // 4. 角色默认权限集合 + if (role && role.permissions.includes(permKey)) { + return true; + } + + return false; +} + +/** + * 交付物可见性 5 步决策流水线 + * 1. private 个人私密文档:严格隔离,仅创建者本人可见(绝对红线) + * 2. shared 项目协作资料:项目全员可见 + * 3. 管理员特权:ADMIN / perm:manage 全员可见 + * 4. 三权法定责任人保底:编制人、审核人、批准人必定可见 + * 5. 单文档覆盖规则 (Override Priority): + * - designated_only: 仅责任人可见 (非责任人已在步骤4排除,直接拒绝) + * - role_whitelist: 仅白名单内角色可见 + * - public: 全员可见 + * 6. 回退继承工作流基线:用户角色需具备 workflow:view_all 权限 + */ +export function isDeliverableVisible( + user: UserProfile, + roles: RoleDefinition[], + doc: DeliverableItem +): boolean { + // 步骤 1:个人私密文档 (private) 绝对隔离 + if (doc.category === 'private') { + return doc.creatorId === user.id; + } + + // 步骤 2:项目协作参考库 (shared) 对项目成员全员可见 + if (doc.category === 'shared') { + return true; + } + + // 步骤 3:管理员超级特权 + const role = roles.find((r) => r.id === user.roleId); + if (role?.code === 'ADMIN' || role?.permissions.includes('perm:manage')) { + return true; + } + + // 步骤 4:法定三权责任人保底 (防止指派后本人看不见无法履责) + const isAssignee = + doc.author === user.name || + doc.reviewer === user.name || + doc.approver === user.name; + if (isAssignee) { + return true; + } + + // 步骤 5:单文档覆盖策略判定 + const rule = doc.visibilityRule; + if (rule && rule.mode !== 'inherit') { + // 5.1 特例人员白名单 (特聘临床专家等) + if (rule.allowedUserIds?.includes(user.id)) { + return true; + } + + switch (rule.mode) { + case 'designated_only': + // 步骤 4 已经排除了非责任人,因此此处直接拒绝 + return false; + + case 'role_whitelist': + // 判定当前用户全局角色是否在白名单中 + return Boolean(user.roleId && rule.allowedRoleIds?.includes(user.roleId)); + + case 'public': + // 放宽为全员公开可见 + return true; + + default: + break; + } + } + + // 步骤 6:回退继承工作流默认基线规则 + return hasPermission(user, roles, 'workflow:view_all'); +} + +/** + * 批量过滤大盘交付物 + */ +export function filterVisibleDeliverablesWithRBAC( + deliverables: DeliverableItem[], + user: UserProfile, + roles: RoleDefinition[] +): DeliverableItem[] { + return deliverables.filter((doc) => isDeliverableVisible(user, roles, doc)); +} + +/** + * 核心业务动作权限拦截判定 + */ +export function canPerformDocAction( + action: 'assign_lead' | 'override_visibility' | 'create_project', + user: UserProfile, + roles: RoleDefinition[] +): boolean { + switch (action) { + case 'create_project': + return hasPermission(user, roles, 'project:create'); + case 'assign_lead': + return hasPermission(user, roles, 'project:assign_lead'); + case 'override_visibility': + return ( + hasPermission(user, roles, 'doc:override_visibility') || + hasPermission(user, roles, 'perm:manage') + ); + default: + return false; + } +} From b13a03840cc003bafdfc1ada6b3cebb95db407f9 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:33:41 +0800 Subject: [PATCH 05/18] feat(web): upgrade settings modal with dynamic roles CRUD and permission matrix Co-authored-by: Cursor --- web/src/components/user/SettingsModal.tsx | 836 +++++++++++++++++++--- web/src/data/storage.ts | 20 + 2 files changed, 747 insertions(+), 109 deletions(-) diff --git a/web/src/components/user/SettingsModal.tsx b/web/src/components/user/SettingsModal.tsx index d239da84..fa2a97dd 100644 --- a/web/src/components/user/SettingsModal.tsx +++ b/web/src/components/user/SettingsModal.tsx @@ -1,12 +1,108 @@ -import { FC, useEffect } from 'react'; -import { X, Settings, Database, Users, Cpu, FileCheck } from 'lucide-react'; +import { FC, useState, useEffect } from 'react'; +import { + X, + Settings, + Shield, + Plus, + Trash2, + Check, + UserCheck, + Users, + Key, + Info, + CheckCircle2, +} from 'lucide-react'; +import { RoleDefinition, PermissionKey } from '../../types/rbac'; +import { UserProfile } from '../../types/docgov'; -interface SettingsModalProps { +export interface SettingsModalProps { isOpen: boolean; onClose: () => void; + roles: RoleDefinition[]; + onSaveRoles: (roles: RoleDefinition[]) => void; + users: UserProfile[]; + currentUser: UserProfile; + onUpdateUserRole: (userId: number, roleId: string) => void; + onSwitchUser: (user: UserProfile) => void; } -export const SettingsModal: FC = ({ isOpen, onClose }) => { +const ALL_PERMISSION_ITEMS: { + key: PermissionKey; + label: string; + desc: string; + category: '项目与流程' | '权限与安全' | '文档与导出'; +}[] = [ + { + key: 'project:create', + label: '创建新项目/立项', + desc: '允许在左侧栏发起新研发项目与阶段模板克隆', + category: '项目与流程', + }, + { + key: 'project:assign_lead', + label: '设置/变更文档责任人', + desc: '允许指派或变更文档的法定编制人、审核人与批准人', + category: '项目与流程', + }, + { + key: 'perm:manage', + label: '系统权限与角色管理', + desc: '超级管理特权,可增删改角色库并配置全局权限', + category: '权限与安全', + }, + { + key: 'workflow:config_visibility', + label: '配置工作流默认可见性', + desc: '允许配置整个研发阶段或工作流的基线可见性规则', + category: '权限与安全', + }, + { + key: 'doc:override_visibility', + label: '配置单文档可见性覆盖', + desc: '允许对敏感或涉密文档设置独立可见性(覆盖工作流默认)', + category: '权限与安全', + }, + { + key: 'workflow:view_all', + label: '查阅常规受控工作流文档', + desc: '默认是否可浏览项目内的受控工作流核心交付物', + category: '文档与导出', + }, + { + key: 'doc:export', + label: '导出受控成果物 (DOCX/XLSX)', + desc: '允许将交付物编译导出为合规 DOCX 报告或测算 XLSX 表格', + category: '文档与导出', + }, +]; + +export const SettingsModal: FC = ({ + isOpen, + onClose, + roles, + onSaveRoles, + users, + currentUser, + onUpdateUserRole, + onSwitchUser, +}) => { + const [activeTab, setActiveTab] = useState<'roles' | 'users'>('roles'); + const [selectedRoleId, setSelectedRoleId] = useState(roles[0]?.id || 'role-admin'); + const [editRoles, setEditRoles] = useState(roles); + const [isCreatingRole, setIsCreatingRole] = useState(false); + const [newRoleName, setNewRoleName] = useState(''); + const [newRoleCode, setNewRoleCode] = useState(''); + const [newRoleDesc, setNewRoleDesc] = useState(''); + const [saveSuccessTip, setSaveSuccessTip] = useState(false); + + // 同步外部 roles + useEffect(() => { + setEditRoles(roles); + if (!roles.some((r) => r.id === selectedRoleId) && roles.length > 0) { + setSelectedRoleId(roles[0].id); + } + }, [roles]); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen) { @@ -19,6 +115,72 @@ export const SettingsModal: FC = ({ isOpen, onClose }) => { if (!isOpen) return null; + const currentSelectedRole = + editRoles.find((r) => r.id === selectedRoleId) || editRoles[0]; + + const handleTogglePermission = (permKey: PermissionKey) => { + if (!currentSelectedRole || currentSelectedRole.isSystem) return; + + const exists = currentSelectedRole.permissions.includes(permKey); + const updatedPermissions = exists + ? currentSelectedRole.permissions.filter((k) => k !== permKey) + : [...currentSelectedRole.permissions, permKey]; + + const updatedRoles = editRoles.map((r) => + r.id === currentSelectedRole.id + ? { ...r, permissions: updatedPermissions, updatedAt: new Date().toLocaleString() } + : r + ); + + setEditRoles(updatedRoles); + onSaveRoles(updatedRoles); + showSavedNotification(); + }; + + const handleCreateRole = () => { + if (!newRoleName.trim() || !newRoleCode.trim()) return; + + const newId = `role-custom-${Date.now()}`; + const newRole: RoleDefinition = { + id: newId, + name: newRoleName.trim(), + code: newRoleCode.trim().toUpperCase(), + description: newRoleDesc.trim() || '自定义业务角色', + isSystem: false, + permissions: ['workflow:view_all', 'doc:export'], + createdAt: new Date().toLocaleString(), + updatedAt: new Date().toLocaleString(), + }; + + const nextRoles = [...editRoles, newRole]; + setEditRoles(nextRoles); + onSaveRoles(nextRoles); + setSelectedRoleId(newId); + setIsCreatingRole(false); + setNewRoleName(''); + setNewRoleCode(''); + setNewRoleDesc(''); + showSavedNotification(); + }; + + const handleDeleteRole = (roleId: string) => { + const roleToDelete = editRoles.find((r) => r.id === roleId); + if (!roleToDelete || roleToDelete.isSystem) return; + + if (window.confirm(`确定要删除角色【${roleToDelete.name}】吗?删除后该角色用户将自动退回普通工程师。`)) { + const nextRoles = editRoles.filter((r) => r.id !== roleId); + setEditRoles(nextRoles); + onSaveRoles(nextRoles); + setSelectedRoleId(nextRoles[0]?.id || 'role-admin'); + showSavedNotification(); + } + }; + + const showSavedNotification = () => { + setSaveSuccessTip(true); + setTimeout(() => setSaveSuccessTip(false), 2000); + }; + return (
= ({ isOpen, onClose }) => { >
= ({ isOpen, onClose }) => { animation: 'scaleIn 0.18s ease-out', }} > + {/* 顶部标题栏 */}
= ({ isOpen, onClose }) => { width: '34px', height: '34px', borderRadius: '8px', - backgroundColor: 'rgba(148, 163, 184, 0.15)', + backgroundColor: 'rgba(79, 107, 255, 0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', - color: 'var(--text-strong)', + color: 'var(--brand-500)', }} >
- 系统设置与管理控制台 (规划预留) + 系统权限与角色管理控制台
- 水木东方医疗器械智能协同工作台 · 系统配置管理 + 全局动态角色 CRUD · 权限配置矩阵 · 人员角色委派与实测切换
+ +
+ {saveSuccessTip && ( + + 已实时自动保存 + + )} + +
+
+ + {/* 标签栏导航 */} +
+ +
-
-
- 本入口已为您保留,后续迭代将在此接入管理员全局管理界面与跨系统配置服务: -
+ {/* 内容区 */} +
+ {activeTab === 'roles' ? ( +
+ {/* 左侧角色列表 */} +
+
+ + 系统全局角色 ({editRoles.length}) + + +
-
-
-
- - - 组织与人员权限 - + {/* 新增角色输入面板 */} + {isCreatingRole && ( +
+ setNewRoleName(e.target.value)} + style={{ + padding: '6px 8px', + fontSize: '12px', + borderRadius: '4px', + border: '1px solid var(--border)', + background: 'var(--bg-panel)', + color: 'var(--text-strong)', + }} + /> + setNewRoleCode(e.target.value)} + style={{ + padding: '6px 8px', + fontSize: '12px', + borderRadius: '4px', + border: '1px solid var(--border)', + background: 'var(--bg-panel)', + color: 'var(--text-strong)', + }} + /> +
+ + +
+
+ )} + + {/* 角色卡片列表 */} +
+ {editRoles.map((role) => { + const isSelected = role.id === selectedRoleId; + return ( +
setSelectedRoleId(role.id)} + style={{ + padding: '10px 12px', + borderRadius: '6px', + marginBottom: '6px', + cursor: 'pointer', + backgroundColor: isSelected ? 'var(--bg-active, #1F2837)' : 'transparent', + border: isSelected ? '1px solid var(--brand-500)' : '1px solid transparent', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + transition: 'all 0.15s ease', + }} + > +
+
+ + {role.name} + + {role.isSystem && ( + + SYSTEM + + )} +
+
+ 代码: {role.code} · {role.permissions.length} 项权限 +
+
+ + {!role.isSystem && ( + + )} +
+ ); + })} +
-
- 配置全公司研发与质量人员库、固定职位及各项目阶段团队角色授权。 + + {/* 右侧权限配置矩阵 */} +
+ {currentSelectedRole && ( + <> +
+
+
+ {currentSelectedRole.name} ({currentSelectedRole.code}) +
+ {currentSelectedRole.isSystem && ( + + 🛡️ 系统内置超级管理员,默认具备所有权限且防误删 + + )} +
+
+ {currentSelectedRole.description} +
+
+ +
+
+ + 默认赋予的原子功能权限项 (勾选即刻生效): +
+ +
+ {ALL_PERMISSION_ITEMS.map((item) => { + const isChecked = + currentSelectedRole.isSystem || + currentSelectedRole.permissions.includes(item.key); + const isDisabled = currentSelectedRole.isSystem; + + return ( +
{ + if (!isDisabled) handleTogglePermission(item.key); + }} + style={{ + padding: '10px 14px', + borderRadius: '8px', + border: '1px solid var(--border)', + backgroundColor: isChecked ? 'rgba(79, 107, 255, 0.05)' : 'var(--bg-subtle)', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + cursor: isDisabled ? 'default' : 'pointer', + transition: 'all 0.15s ease', + }} + > +
+
+ + {item.label} + + + {item.category} + +
+
+ {item.key} · {item.desc} +
+
+ +
+ {isChecked && } +
+
+ ); + })} +
+
+ + )}
- + ) : ( + /* Tab 2: 人员角色委派与当前登录人切换 */
-
- - - 法规知识库索引 +
+ + + 在此为研发中心人员委派全局角色。支持一键将当前会话登录人切换为不同角色身份,快速实测“新建项目”、“指派负责人”及“单文档可见性覆盖”的受控拦截效果。
-
- 管理 NMPA、FDA、CE-MDR 监管法规库与 pgvector 语义嵌入向量同步更新。 -
-
-
-
- - - 多智能体编排与模型 - -
-
- 配置法规专家、方案总师、CDO报价师与评审参谋的提示词与大模型网关。 -
-
+
+ {users.map((u) => { + const isCurrent = u.id === currentUser.id; + const userRoleId = u.roleId || 'role-engineer'; + const userRole = editRoles.find((r) => r.id === userRoleId); -
-
- - - 合规审计与电子签名 - -
-
- 遵循 21 CFR Part 11 与 GMP 电子签名审计记录、哈希防篡改检验。 + return ( +
+
+
+ {u.name.slice(0, 1)} +
+
+
+ + {u.name} + + + 工号: {u.employeeId} + + {isCurrent && ( + + 当前登录身份 + + )} +
+
+ {u.department} · {u.position} +
+
+
+ +
+ {/* 角色委派下拉框 */} +
+ 委派全局角色: + +
+ + {/* 切换登录人按钮 */} + {!isCurrent ? ( + + ) : ( + + ✓ 正在以此身份交互 + + )} +
+
+ ); + })}
-
+ )}
+ {/* 底部按钮栏 */}
+ + 水木东方医疗器械智能协同工作台 · 全生命周期权限治理 +
diff --git a/web/src/data/storage.ts b/web/src/data/storage.ts index ae778ae2..47658e2f 100644 --- a/web/src/data/storage.ts +++ b/web/src/data/storage.ts @@ -1,4 +1,6 @@ import { ProjectSession, ChatMessage, DeliverableItem } from '../types/workbench'; +import { RoleDefinition } from '../types/rbac'; +import { UserProfile } from '../types/docgov'; const STORAGE_PREFIX = 'smdf_agent_workbench_'; @@ -8,6 +10,8 @@ export const STORAGE_KEYS = { MESSAGES: `${STORAGE_PREFIX}messages`, DELIVERABLES: `${STORAGE_PREFIX}deliverables`, DOC_CACHE: `${STORAGE_PREFIX}doc_cache`, + ROLES: `${STORAGE_PREFIX}roles`, + USERS: `${STORAGE_PREFIX}users`, }; /** @@ -168,6 +172,22 @@ export function saveDocCache(cache: Record): void { safeSetItem(STORAGE_KEYS.DOC_CACHE, cache); } +export function loadRoles(fallback: RoleDefinition[]): RoleDefinition[] { + return safeGetItem(STORAGE_KEYS.ROLES, fallback); +} + +export function saveRoles(roles: RoleDefinition[]): void { + safeSetItem(STORAGE_KEYS.ROLES, roles); +} + +export function loadUsers(fallback: UserProfile[]): UserProfile[] { + return safeGetItem(STORAGE_KEYS.USERS, fallback); +} + +export function saveUsers(users: UserProfile[]): void { + safeSetItem(STORAGE_KEYS.USERS, users); +} + /** * resetWorkbenchStorage: 清空工作台缓存,恢复系统默认体验状态 */ From d905c91338744c5ed35aa94f408fa5102234bd1c Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:36:55 +0800 Subject: [PATCH 06/18] feat(web): add doc visibility override modal and badges in deck drawer Co-authored-by: Cursor --- web/src/components/doc/DeckDrawer.tsx | 42 +- web/src/components/doc/DocGovActionBar.tsx | 80 +++ web/src/components/doc/DocVisibilityModal.tsx | 464 ++++++++++++++++++ web/src/types/block.ts | 2 + 4 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 web/src/components/doc/DocVisibilityModal.tsx diff --git a/web/src/components/doc/DeckDrawer.tsx b/web/src/components/doc/DeckDrawer.tsx index 157a1257..4780463f 100644 --- a/web/src/components/doc/DeckDrawer.tsx +++ b/web/src/components/doc/DeckDrawer.tsx @@ -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 = ({ @@ -40,6 +42,7 @@ export const DeckDrawer: React.FC = ({ onShareToProject, onRevokeShare, onDeleteDoc, + onOpenDocVisibility, }) => { const [filterCategory, setFilterCategory] = useState<'all' | DeliverableCategory>('all'); @@ -180,6 +183,40 @@ export const DeckDrawer: React.FC = ({ } }; + const renderVisibilityBadge = (rule?: DocVisibilityRule) => { + if (!rule || rule.mode === 'inherit') return null; + const isDesignated = rule.mode === 'designated_only'; + const isWhitelist = rule.mode === 'role_whitelist'; + + return ( + + + {isDesignated ? '仅责任人' : isWhitelist ? '指定角色' : '公开'} + + ); + }; + 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 = ({ {doc.title} - {renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)} +
+ {renderVisibilityBadge(doc.visibilityRule)} + {renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)} +
diff --git a/web/src/components/doc/DocGovActionBar.tsx b/web/src/components/doc/DocGovActionBar.tsx index a3cbf3aa..856800d8 100644 --- a/web/src/components/doc/DocGovActionBar.tsx +++ b/web/src/components/doc/DocGovActionBar.tsx @@ -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 = ({ ast, currentRole, currentUserId, + canOverrideVisibility = true, + onOpenVisibilityModal, onClaim, onSubmitReview, onPassReview, @@ -315,10 +319,86 @@ export const DocGovActionBar: React.FC = ({ • 已被编制人认领 )} + {ast.visibilityRule && ast.visibilityRule.mode !== 'inherit' && ( + + + + {ast.visibilityRule.mode === 'designated_only' + ? '仅责任人可见' + : ast.visibilityRule.mode === 'role_whitelist' + ? '角色白名单' + : '全员公开'} + + + )}
{/* 右侧动作流转按钮 */}
+ {onOpenVisibilityModal && ( + + )} {/* 1. 编制阶段 */} {ast.lifecycleState === 'drafting' && ( <> diff --git a/web/src/components/doc/DocVisibilityModal.tsx b/web/src/components/doc/DocVisibilityModal.tsx new file mode 100644 index 00000000..e761a8dc --- /dev/null +++ b/web/src/components/doc/DocVisibilityModal.tsx @@ -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 = ({ + isOpen, + onClose, + doc, + roles, + currentUserName, + onSaveRule, +}) => { + const [mode, setMode] = useState('inherit'); + const [selectedRoleIds, setSelectedRoleIds] = useState([]); + const [overrideReason, setOverrideReason] = useState(''); + + // 初始化已有规则 + 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 ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
+ {/* 顶部标题栏 */} +
+
+
+ +
+
+
+ 单文档可见性覆盖策略 (Doc Visibility Override) +
+
+ 设置后将优先于工作流默认可见性规则生效 +
+
+
+ +
+ + {/* 主体表单 */} +
+ {/* 目标文档信息卡 */} +
+
+ + + {doc.title} + +
+
+ 所属阶段: {doc.stage} + 编制: {doc.author} + 审核: {doc.reviewer} + 批准: {doc.approver} +
+
+ + {/* 覆盖模式选择 */} +
+ + 选择可见性覆盖策略: + + + {/* 模式 1: 继承工作流默认 */} + + + {/* 模式 2: 仅三权责任人可见 */} + + + {/* 模式 3: 指定全局角色白名单 */} + + + {/* 模式 4: 全员公开 */} + +
+ + {/* 覆盖原因留痕输入 */} + {mode !== 'inherit' && ( +
+ + 覆盖原因与合规留痕说明: + + 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)', + }} + /> +
+ )} +
+ + {/* 底部操作栏 */} +
+ {doc.visibilityRule && doc.visibilityRule.mode !== 'inherit' ? ( + + ) : ( + + 当前状态:继承工作流默认可见性 + + )} + +
+ + +
+
+
+
+ ); +}; diff --git a/web/src/types/block.ts b/web/src/types/block.ts index da4e8171..6a7e6f96 100644 --- a/web/src/types/block.ts +++ b/web/src/types/block.ts @@ -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; } From e1c1d03a858fa33fd63268df5a42aa0a23ae0da8 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 22:40:22 +0800 Subject: [PATCH 07/18] feat(web): wire rbac permission enforcement into left rail and rights modal Co-authored-by: Cursor --- web/src/App.tsx | 144 ++++++++++- web/src/components/layout/AppShell.tsx | 3 + web/src/components/layout/LeftRail.tsx | 19 +- .../user/UserProjectRightsModal.tsx | 12 +- web/src/engine/rbacIntegration.test.ts | 224 ++++++++++++++++++ 5 files changed, 384 insertions(+), 18 deletions(-) create mode 100644 web/src/engine/rbacIntegration.test.ts 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 = ({ {/* 顶部操作区 */}
+
+ +
+ +
+ + setNewPassword(e.target.value)} + placeholder="至少 6 位字符" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-amber-500" + /> +
+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="再次输入新密码" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-amber-500" + /> +
+
+ +
+ + + +
+ +
+
+ ); +}; diff --git a/web/src/components/auth/LoginView.tsx b/web/src/components/auth/LoginView.tsx new file mode 100644 index 00000000..720994b6 --- /dev/null +++ b/web/src/components/auth/LoginView.tsx @@ -0,0 +1,419 @@ +import React, { useState } from 'react'; +import { + ShieldCheck, + Lock, + User, + Building, + BadgeAlert, + ArrowRight, + UserPlus, + LogIn, + CheckCircle2, + AlertCircle, + Sparkles, +} from 'lucide-react'; +import { SystemAuthConfig, LoginResult } from '../../types/auth'; +import { authClient } from '../../api/auth'; + +interface LoginViewProps { + onLoginSuccess: (session: LoginResult) => void; + systemConfig?: SystemAuthConfig; + env?: string; +} + +export const LoginView: React.FC = ({ + onLoginSuccess, + systemConfig = { allowRegistration: true, requireApproval: false }, + env = 'dev', +}) => { + const [activeTab, setActiveTab] = useState<'login' | 'register'>('login'); + + // Login form state + const [loginUsername, setLoginUsername] = useState(''); + const [loginPassword, setLoginPassword] = useState(''); + const [loginLoading, setLoginLoading] = useState(false); + const [loginError, setLoginError] = useState(''); + + // Register form state (Minimal: username, password, name. Optional: dept, empId) + const [regUsername, setRegUsername] = useState(''); + const [regPassword, setRegPassword] = useState(''); + const [regConfirmPassword, setRegConfirmPassword] = useState(''); + const [regName, setRegName] = useState(''); + const [regDepartment, setRegDepartment] = useState(''); + const [regEmployeeId, setRegEmployeeId] = useState(''); + const [regLoading, setRegLoading] = useState(false); + const [regError, setRegError] = useState(''); + const [regSuccessMsg, setRegSuccessMsg] = useState(''); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + if (!loginUsername.trim() || !loginPassword) { + setLoginError('请输入用户名和密码'); + return; + } + setLoginLoading(true); + setLoginError(''); + try { + const session = await authClient.login({ + username: loginUsername.trim(), + password: loginPassword, + }); + onLoginSuccess(session); + } catch (err: any) { + setLoginError(err.message || '登录失败,请检查账号密码'); + } finally { + setLoginLoading(false); + } + }; + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + setRegError(''); + setRegSuccessMsg(''); + + if (!regUsername.trim()) { + setRegError('请输入用户名'); + return; + } + if (!regPassword) { + setRegError('请输入密码'); + return; + } + if (regPassword !== regConfirmPassword) { + setRegError('两次输入的密码不一致'); + return; + } + if (!regName.trim()) { + setRegError('请输入真实姓名'); + return; + } + + setRegLoading(true); + try { + const res = await authClient.register({ + username: regUsername.trim(), + password: regPassword, + name: regName.trim(), + department: regDepartment.trim() || undefined, + employeeId: regEmployeeId.trim() || undefined, + }); + + setRegSuccessMsg(res.message); + // 预填登录框 + setLoginUsername(regUsername.trim()); + setLoginPassword(''); + // 若无需审核,可延迟切回登录 + setTimeout(() => { + setActiveTab('login'); + }, 1800); + } catch (err: any) { + setRegError(err.message || '注册申请提交失败'); + } finally { + setRegLoading(false); + } + }; + + return ( +
+ {/* 顶部环境标识 */} +
+ + 水木东方医疗智脑 · 研发认证中心 + + ENV: {env.toUpperCase()} + +
+ + {/* 主登录卡片 */} +
+ {/* 系统徽章与主标 */} +
+
+ +
+

+ 水木东方协同研发系统 +

+

+ 高端医疗器械受控研发平台 · 认证与权限受控网关 +

+
+ + {/* Tab 导航切换 */} +
+ + + {systemConfig.allowRegistration && ( + + )} +
+ + {/* 1. 登录表单 */} + {activeTab === 'login' && ( +
+ {loginError && ( +
+ + {loginError} +
+ )} + +
+ +
+ + setLoginUsername(e.target.value)} + placeholder="例如: admin 或 zhanggong" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl pl-9 pr-3 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition" + /> +
+
+ +
+ +
+ + setLoginPassword(e.target.value)} + placeholder="••••••••" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl pl-9 pr-3 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition" + /> +
+
+ + + + {/* 快捷测试账号引导 */} +
+
+ + 开发环境常用凭据: +
+
+ + + +
+
+
+ )} + + {/* 2. 注册表单 */} + {activeTab === 'register' && ( +
+ {regError && ( +
+ + {regError} +
+ )} + + {regSuccessMsg && ( +
+ + {regSuccessMsg} +
+ )} + +
+
+ + setRegName(e.target.value)} + placeholder="例如: 赵工" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl px-3 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500" + /> +
+ +
+ + setRegUsername(e.target.value)} + placeholder="英文字符/数字" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl px-3 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500" + /> +
+
+ +
+
+ + setRegPassword(e.target.value)} + placeholder="不少于6位" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl px-3 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500" + /> +
+ +
+ + setRegConfirmPassword(e.target.value)} + placeholder="再次输入密码" + className="w-full bg-slate-950 border border-slate-700/80 rounded-xl px-3 py-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500" + /> +
+
+ + {/* 可选项:部门与工号 */} +
+
+ +
+ + setRegDepartment(e.target.value)} + placeholder="如: 研发中心" + className="w-full bg-slate-950/70 border border-slate-800 rounded-xl pl-8 pr-2.5 py-1.5 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:border-slate-600" + /> +
+
+ +
+ + setRegEmployeeId(e.target.value)} + placeholder="如: RD-088" + className="w-full bg-slate-950/70 border border-slate-800 rounded-xl px-2.5 py-1.5 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:border-slate-600" + /> +
+
+ + {/* 核心治理规则提醒:职位必须由管理员指定 */} +
+ +
+ 受控职位安全准则: + + 用户不可自行填选职位与角色。自主注册将分配为待指派访客,由管理员在后台统一设定受控职位与项目权限。 + +
+
+ + +
+ )} +
+ + {/* 底部版权与合规标识 */} +
+ 水木东方(北京)医疗科技有限公司 · 医疗器械受控数据治理与 AI 参谋平台 +
+
+ ); +}; diff --git a/web/src/components/doc/DeckDrawer.tsx b/web/src/components/doc/DeckDrawer.tsx index 4780463f..3c8be9e4 100644 --- a/web/src/components/doc/DeckDrawer.tsx +++ b/web/src/components/doc/DeckDrawer.tsx @@ -183,13 +183,14 @@ export const DeckDrawer: React.FC = ({ } }; - const renderVisibilityBadge = (rule?: DocVisibilityRule) => { + const renderVisibilityBadge = (rule?: DocVisibilityRule, onClick?: (e: React.MouseEvent) => void) => { if (!rule || rule.mode === 'inherit') return null; const isDesignated = rule.mode === 'designated_only'; const isWhitelist = rule.mode === 'role_whitelist'; return ( = ({ display: 'inline-flex', alignItems: 'center', gap: '3px', + cursor: onClick ? 'pointer' : 'default', }} title={`覆盖原因: ${rule.overrideReason || '权限覆盖模式'}`} > @@ -449,7 +451,10 @@ export const DeckDrawer: React.FC = ({ {doc.title}
- {renderVisibilityBadge(doc.visibilityRule)} + {renderVisibilityBadge(doc.visibilityRule, (e) => { + e.stopPropagation(); + onOpenDocVisibility?.(doc); + })} {renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)}
diff --git a/web/src/components/doc/DocVisibilityModal.tsx b/web/src/components/doc/DocVisibilityModal.tsx index e761a8dc..913924cd 100644 --- a/web/src/components/doc/DocVisibilityModal.tsx +++ b/web/src/components/doc/DocVisibilityModal.tsx @@ -1,12 +1,9 @@ import { FC, useState, useEffect } from 'react'; import { X, - ShieldAlert, Lock, Globe, Users, - CheckCircle2, - AlertCircle, FileText, } from 'lucide-react'; import { DeliverableItem } from '../../types/workbench'; diff --git a/web/src/components/user/SettingsModal.tsx b/web/src/components/user/SettingsModal.tsx index fa2a97dd..9bea8bbf 100644 --- a/web/src/components/user/SettingsModal.tsx +++ b/web/src/components/user/SettingsModal.tsx @@ -694,7 +694,6 @@ export const SettingsModal: FC = ({ {users.map((u) => { const isCurrent = u.id === currentUser.id; const userRoleId = u.roleId || 'role-engineer'; - const userRole = editRoles.find((r) => r.id === userRoleId); return (
= { admin: { diff --git a/web/src/types/auth.ts b/web/src/types/auth.ts index f83ee409..9dd9561a 100644 --- a/web/src/types/auth.ts +++ b/web/src/types/auth.ts @@ -6,6 +6,7 @@ export interface UserAccount extends UserProfile { username: string; status: AccountStatus; mustChangePassword?: boolean; + email?: string; createdAt?: string; updatedAt?: string; } From 775bf73fb12c4defe83f425d6a27674fae71b201 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 23:24:47 +0800 Subject: [PATCH 15/18] feat(web): implement dedicated UserSettingsModal Co-authored-by: Cursor --- web/src/components/user/UserSettingsModal.tsx | 518 ++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 web/src/components/user/UserSettingsModal.tsx diff --git a/web/src/components/user/UserSettingsModal.tsx b/web/src/components/user/UserSettingsModal.tsx new file mode 100644 index 00000000..00c8e6e0 --- /dev/null +++ b/web/src/components/user/UserSettingsModal.tsx @@ -0,0 +1,518 @@ +import React, { useState } from 'react'; +import { + User, + Shield, + KeyRound, + LogOut, + Check, + X, + AlertCircle, + CheckCircle2, + Lock, + FileText, + Sliders, +} from 'lucide-react'; +import { UserProfile } from '../../types/docgov'; +import { RoleDefinition } from '../../types/rbac'; +import { authClient } from '../../api/auth'; + +export interface UserSettingsModalProps { + isOpen: boolean; + onClose: () => void; + currentUser: UserProfile; + roles: RoleDefinition[]; + onUpdateProfile: (updated: Partial) => void; + onLogout: () => void; + theme?: 'dark' | 'light'; + onToggleTheme?: () => void; +} + +const ALL_PERMISSION_DESCRIPTIONS: { key: string; name: string; desc: string }[] = [ + { key: 'project:create', name: '创建新项目/立项', desc: '具备在协同工作台开启全新研发项目并指派阶段的权限' }, + { key: 'project:assign_lead', name: '设置文档责任人', desc: '在文档治理面板为交付物指派编制人、审核人与批准人' }, + { key: 'perm:manage', name: '系统权限与角色管理', desc: '增删改全局职位角色定义与权限矩阵 (管理员独占)' }, + { key: 'workflow:config_visibility', name: '配置工作流默认可见性', desc: '调整业务阶段中各受控交付物的基线查阅准入边界' }, + { key: 'doc:override_visibility', name: '配置单文档可见性覆盖', desc: '针对特定核心机密交付物设定独占白名单或责任人收紧' }, + { key: 'doc:export', name: '受控交付物导出', desc: '允许将医疗受控文档编译导出为受保护的 DOCX 或 XLSX' }, + { key: 'workflow:view_all', name: '默认查阅常规工作流文档', desc: '允许在全景大盘中查阅已就绪的常规项目协作文档' }, +]; + +export const UserSettingsModal: React.FC = ({ + isOpen, + onClose, + currentUser, + roles, + onUpdateProfile, + onLogout, + theme = 'dark', + onToggleTheme, +}) => { + const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences' | 'permissions'>('profile'); + + // Profile form state + const [name, setName] = useState(currentUser.name); + const [department, setDepartment] = useState(currentUser.department); + const [employeeId] = useState(currentUser.employeeId); + const [profileSuccessTip, setProfileSuccessTip] = useState(false); + + // Security password state + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [passwordSuccessTip, setPasswordSuccessTip] = useState(false); + const [passwordLoading, setPasswordLoading] = useState(false); + + // Preferences + const [exportFormat, setExportFormat] = useState<'docx' | 'xlsx'>('docx'); + + if (!isOpen) return null; + + // 获取当前用户的角色对象 + const currentRole = roles.find((r) => r.id === (currentUser.roleId || 'role-engineer')); + const userPermissions = new Set(currentRole?.permissions || []); + if (currentUser.roleId === 'role-admin' || currentRole?.code === 'ADMIN') { + ALL_PERMISSION_DESCRIPTIONS.forEach((p) => userPermissions.add(p.key)); + } + + const handleSaveProfile = (e: React.FormEvent) => { + e.preventDefault(); + onUpdateProfile({ + name: name.trim(), + department: department.trim(), + }); + setProfileSuccessTip(true); + setTimeout(() => setProfileSuccessTip(false), 2500); + }; + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + setPasswordError(''); + setPasswordSuccessTip(false); + + if (!oldPassword) { + setPasswordError('请输入当前原密码'); + return; + } + if (!newPassword || newPassword.length < 6) { + setPasswordError('新密码长度不能少于 6 位'); + return; + } + if (newPassword !== confirmPassword) { + setPasswordError('两次输入的新密码不一致'); + return; + } + + setPasswordLoading(true); + try { + await authClient.changePassword(currentUser.id, oldPassword, newPassword); + setPasswordSuccessTip(true); + setOldPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setTimeout(() => setPasswordSuccessTip(false), 3000); + } catch (err: any) { + setPasswordError(err.message || '密码修改失败'); + } finally { + setPasswordLoading(false); + } + }; + + return ( +
+
+ {/* 顶栏 */} +
+
+
+ +
+
+

+ 个人中心与偏好设置 + + {currentUser.name} + +

+

+ 管理您的个人资料、登录安全凭证及操作偏好 +

+
+
+ +
+ + {/* 主体左右分栏 */} +
+ {/* 左侧菜单 Tab */} +
+
+ + + + + + + +
+ + {/* 底部退出登录 */} +
+ +
+
+ + {/* 右侧内容面板 */} +
+ {/* 1. 个人资料 */} + {activeTab === 'profile' && ( +
+
+
+

基础档案

+

查看工号与职位,完善姓名与部门信息

+
+ {profileSuccessTip && ( + + 资料已保存 + + )} +
+ +
+
+ + setName(e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-100 focus:outline-none focus:border-cyan-500" + /> +
+ +
+ + +
+
+ +
+
+ + setDepartment(e.target.value)} + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-100 focus:outline-none focus:border-cyan-500" + /> +
+ +
+ +
+ {currentUser.position} + + {currentRole?.name || '方案工程师'} + +
+
+
+ + {/* 职位受控说明 */} +
+ + + 您的职位与权限由系统管理员在后台统一设定与审核,个人不可调整。如需变更职位或增加权限,请联系管理员。 + +
+ +
+ +
+
+ )} + + {/* 2. 安全与改密 */} + {activeTab === 'security' && ( +
+
+

修改登录密码

+

定期更新密码保障受控医疗器械工程研发数据安全

+
+ + {passwordError && ( +
+ + {passwordError} +
+ )} + + {passwordSuccessTip && ( +
+ + 密码已成功更新!请妥善保管新密码。 +
+ )} + +
+ + setOldPassword(e.target.value)} + placeholder="请输入当前密码" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-100 focus:outline-none focus:border-cyan-500" + /> +
+ +
+
+ + setNewPassword(e.target.value)} + placeholder="不少于6位" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-100 focus:outline-none focus:border-cyan-500" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="再次输入新密码" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-100 focus:outline-none focus:border-cyan-500" + /> +
+
+ +
+ +
+
+ )} + + {/* 3. 偏好设定 */} + {activeTab === 'preferences' && ( +
+
+

协同工作台偏好

+

个性化定制您的操作体验与交付物导出配置

+
+ + {/* 主题切换 */} +
+
+ 界面色彩主题 +

切换深色或明亮设计视觉风格

+
+ +
+ + {/* 默认导出格式 */} +
+
+ 受控文档默认导出格式 +

快捷导出受控交付物时的优先格式

+
+
+ + +
+
+
+ )} + + {/* 4. 我的权限清单 */} + {activeTab === 'permissions' && ( +
+
+
+

当前账号权限能力清单

+

+ 基于职位关联角色 [{currentRole?.name}] 分配的 7 项原子权限 +

+
+ + 已获授权: {userPermissions.size} / {ALL_PERMISSION_DESCRIPTIONS.length} + +
+ +
+ {ALL_PERMISSION_DESCRIPTIONS.map((perm) => { + const hasIt = userPermissions.has(perm.key); + return ( +
+
+
+ + {perm.name} + + + {perm.key} + +
+

{perm.desc}

+
+ +
+ {hasIt ? ( + + + 已授权 + + ) : ( + + + 未获得 + + )} +
+
+ ); + })} +
+
+ )} +
+
+
+
+ ); +}; From cd08e443eb026557c305b432938f33ce57b629a6 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 8 Sep 2026 23:26:41 +0800 Subject: [PATCH 16/18] feat(web): implement dedicated AdminSettingsModal for role-admin Co-authored-by: Cursor --- .../components/admin/AdminSettingsModal.tsx | 1066 +++++++++++++++++ 1 file changed, 1066 insertions(+) create mode 100644 web/src/components/admin/AdminSettingsModal.tsx diff --git a/web/src/components/admin/AdminSettingsModal.tsx b/web/src/components/admin/AdminSettingsModal.tsx new file mode 100644 index 00000000..21343c62 --- /dev/null +++ b/web/src/components/admin/AdminSettingsModal.tsx @@ -0,0 +1,1066 @@ +import React, { useState, useEffect } from 'react'; +import { + ShieldAlert, + Users, + UserCheck, + ToggleLeft, + ToggleRight, + Shield, + Server, + Plus, + Lock, + X, + Check, + Ban, + CheckCircle2, + AlertCircle, + Settings2, + Info, +} from 'lucide-react'; +import { UserProfile } from '../../types/docgov'; +import { RoleDefinition, PermissionKey } from '../../types/rbac'; +import { UserAccount, SystemAuthConfig } from '../../types/auth'; +import { authClient } from '../../api/auth'; + +export interface AdminSettingsModalProps { + isOpen: boolean; + onClose: () => void; + currentUser: UserProfile; + roles: RoleDefinition[]; + onSaveRoles: (roles: RoleDefinition[]) => void; + allUsers: UserProfile[]; + onRefreshUsers?: () => void; + env?: string; +} + +const ALL_PERMISSION_ITEMS: { key: PermissionKey; name: string; desc: string }[] = [ + { key: 'project:create', name: '创建新项目/立项', desc: '具备在协同工作台开启全新研发项目并指派阶段的权限' }, + { key: 'project:assign_lead', name: '设置文档责任人', desc: '在文档治理面板为交付物指派编制人、审核人与批准人' }, + { key: 'perm:manage', name: '系统权限与角色管理', desc: '增删改全局职位角色定义与权限矩阵' }, + { key: 'workflow:config_visibility', name: '配置工作流默认可见性', desc: '调整业务阶段中各受控交付物的基线查阅准入边界' }, + { key: 'doc:override_visibility', name: '配置单文档可见性覆盖', desc: '针对特定核心机密交付物设定独占白名单或责任人收紧' }, + { key: 'doc:export', name: '受控交付物导出', desc: '允许将医疗受控文档编译导出为受保护的 DOCX 或 XLSX' }, + { key: 'workflow:view_all', name: '默认查阅常规工作流文档', desc: '允许在全景大盘中查阅已就绪的常规项目协作文档' }, +]; + +export const AdminSettingsModal: React.FC = ({ + isOpen, + onClose, + currentUser, + roles, + onSaveRoles, + allUsers: _allUsers, + onRefreshUsers, + env = 'dev', +}) => { + const [activeTab, setActiveTab] = useState<'users' | 'approvals' | 'policy' | 'rbac' | 'system'>('users'); + + // Accounts state + const [userAccounts, setUserAccounts] = useState([]); + const [loadingUsers, setLoadingUsers] = useState(false); + const [feedbackTip, setFeedbackTip] = useState<{ type: 'success' | 'error'; msg: string } | null>(null); + + // System auth config state + const [systemConfig, setSystemConfig] = useState({ + allowRegistration: true, + requireApproval: false, + }); + + // Create User modal + const [showCreateModal, setShowCreateModal] = useState(false); + const [newUsername, setNewUsername] = useState(''); + const [newName, setNewName] = useState(''); + const [newInitialPassword, setNewInitialPassword] = useState('123456'); + const [newRoleId, setNewRoleId] = useState('role-engineer'); + const [newDepartment, setNewDepartment] = useState('研发中心'); + const [newEmployeeId, setNewEmployeeId] = useState(''); + + // RBAC dynamic role editing state + const [editRoles, setEditRoles] = useState(roles); + const [selectedRoleId, setSelectedRoleId] = useState(roles[0]?.id || 'role-admin'); + const [showNewRoleDialog, setShowNewRoleDialog] = useState(false); + const [newRoleName, setNewRoleName] = useState(''); + const [newRoleCode, setNewRoleCode] = useState(''); + const [newRoleDesc, setNewRoleDesc] = useState(''); + + // 1. 数据装载 + const loadAccounts = async () => { + setLoadingUsers(true); + try { + const list = await authClient.getAdminUsers(); + setUserAccounts(list); + const cfg = await authClient.getSystemAuthConfig(); + setSystemConfig(cfg); + } catch (e: any) { + console.warn('Failed to load admin accounts:', e); + } finally { + setLoadingUsers(false); + } + }; + + useEffect(() => { + if (isOpen) { + loadAccounts(); + setEditRoles(roles); + } + }, [isOpen, roles]); + + if (!isOpen) return null; + + // 严格入口防线:仅系统管理员独占访问 + const isAdmin = currentUser.roleId === 'role-admin' || currentUser.name.includes('管理员'); + if (!isAdmin) { + return ( +
+
+
+ +
+

403 访问被拒绝

+

+ 当前功能为系统管理控制台,仅系统管理员 (role-admin) 具备访问权限。 +

+ +
+
+ ); + } + + const showTip = (type: 'success' | 'error', msg: string) => { + setFeedbackTip({ type, msg }); + setTimeout(() => setFeedbackTip(null), 3000); + }; + + // 职位分配 + const handleAssignRole = async (targetUser: UserAccount, targetRoleId: string) => { + const roleDef = roles.find((r) => r.id === targetRoleId); + const positionName = roleDef ? roleDef.name : '方案工程师'; + try { + await authClient.updateUserRole(targetUser.id, targetRoleId, positionName); + await loadAccounts(); + onRefreshUsers?.(); + showTip('success', `已将 ${targetUser.name} 的职位更新为【${positionName}】`); + } catch (e: any) { + showTip('error', e.message || '更新职位失败'); + } + }; + + // 审批通过并指派职位 + const handleApproveUser = async (targetUser: UserAccount, targetRoleId: string) => { + const roleDef = roles.find((r) => r.id === targetRoleId); + const positionName = roleDef ? roleDef.name : '方案工程师'; + try { + await authClient.updateUserStatus(targetUser.id, 'active', targetRoleId, positionName); + await loadAccounts(); + onRefreshUsers?.(); + showTip('success', `已批准 ${targetUser.name} 入职并指派职位【${positionName}】`); + } catch (e: any) { + showTip('error', e.message || '审批处理失败'); + } + }; + + // 停用/启用账号 + const handleToggleUserStatus = async (targetUser: UserAccount) => { + const nextStatus = targetUser.status === 'active' ? 'disabled' : 'active'; + try { + await authClient.updateUserStatus(targetUser.id, nextStatus); + await loadAccounts(); + onRefreshUsers?.(); + showTip('success', `已将账号 ${targetUser.name} 设置为 ${nextStatus === 'active' ? '正常' : '已停用'}`); + } catch (e: any) { + showTip('error', e.message || '切换状态失败'); + } + }; + + // 管理员新建账号 + const handleCreateUser = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newUsername.trim() || !newName.trim() || !newInitialPassword.trim()) { + showTip('error', '请填写必填字段'); + return; + } + const roleDef = roles.find((r) => r.id === newRoleId); + const positionName = roleDef ? roleDef.name : '方案工程师'; + + try { + await authClient.createAdminUser({ + username: newUsername.trim(), + name: newName.trim(), + initialPassword: newInitialPassword.trim(), + roleId: newRoleId, + position: positionName, + department: newDepartment.trim() || undefined, + employeeId: newEmployeeId.trim() || undefined, + }); + setShowCreateModal(false); + setNewUsername(''); + setNewName(''); + setNewInitialPassword('123456'); + setNewEmployeeId(''); + await loadAccounts(); + onRefreshUsers?.(); + showTip('success', `新员工账号创建成功!已标记为【首次登录必须改密】。`); + } catch (e: any) { + showTip('error', e.message || '创建账号失败'); + } + }; + + // 准入策略切换 + const handleTogglePolicy = async (key: keyof SystemAuthConfig) => { + const updated = { ...systemConfig, [key]: !systemConfig[key] }; + try { + await authClient.updateSystemAuthConfig(updated); + setSystemConfig(updated); + showTip('success', '准入治理策略已实时生效'); + } catch (e: any) { + showTip('error', e.message || '更新策略失败'); + } + }; + + // RBAC 权限勾选 + const handleTogglePermission = (roleId: string, permKey: PermissionKey) => { + setEditRoles((prev) => + prev.map((r) => { + if (r.id !== roleId) return r; + const exists = r.permissions.includes(permKey); + const nextPerms = exists + ? r.permissions.filter((p) => p !== permKey) + : [...r.permissions, permKey]; + return { ...r, permissions: nextPerms, updatedAt: new Date().toISOString() }; + }) + ); + }; + + // 保存 RBAC 角色 + const handleSaveRBAC = () => { + onSaveRoles(editRoles); + showTip('success', '全局角色与权限矩阵已保存并实时广播!'); + }; + + // 待审批列表 + const pendingUsers = userAccounts.filter((u) => u.status === 'pending_approval'); + + const selectedRole = editRoles.find((r) => r.id === selectedRoleId) || editRoles[0]; + + return ( +
+
+ {/* 顶部标题栏 */} +
+
+
+ +
+
+
+

系统管理与治理控制台

+ + 管理员特权独占 + + + ENV: {env.toUpperCase()} + +
+

+ 人员账号审核、受控职位指派、准入开关及全局 RBAC 权限矩阵 +

+
+
+ +
+ {feedbackTip && ( +
+ {feedbackTip.type === 'success' ? ( + + ) : ( + + )} + {feedbackTip.msg} +
+ )} + + +
+
+ + {/* 主体布局 */} +
+ {/* 左侧导航 Tab */} +
+ + + + + + + + + +
+ + {/* 右侧主工作区 */} +
+ {/* 1. 人员与职位治理 */} + {activeTab === 'users' && ( +
+
+
+

研发中心全员账号与职位指派

+

+ 职位对应系统角色,决定其在研发协同工作台中的立项、文档责任人分配与受控操作权限 +

+
+ +
+ + {loadingUsers ? ( +
正在同步系统用户列表...
+ ) : ( +
+ + + + + + + + + + + + {userAccounts.map((u) => { + const isSuperAdmin = u.username === 'admin' || u.roleId === 'role-admin'; + return ( + + + + + + + + + + + + ); + })} + +
姓名 / 用户名部门 / 工号受控职位 (指派角色)账号状态管理操作
+
+ {u.name} + {u.mustChangePassword && ( + + 待首次改密 + + )} +
+
{u.username}
+
+
{u.department || '未录入'}
+
{u.employeeId || '-'}
+
+ {isSuperAdmin ? ( + + 系统管理员 + + ) : ( + + )} + + {u.status === 'active' && ( + + 正常 + + )} + {u.status === 'pending_approval' && ( + + 待审核 + + )} + {u.status === 'disabled' && ( + + 已停用 + + )} + + {!isSuperAdmin && ( +
+ +
+ )} +
+
+ )} +
+ )} + + {/* 2. 注册审批中心 */} + {activeTab === 'approvals' && ( +
+
+

新注册员工审核与职位指派

+

+ 当系统准入策略开启“新员工需管理员审核”时,自主注册的申请人将在此汇聚,待指派正式职位后激活 +

+
+ + {pendingUsers.length === 0 ? ( +
+ +

当前没有待审核的注册申请

+

+ 所有注册员工均已审核激活,或当前准入策略已设置为自动激活 +

+
+ ) : ( +
+ {pendingUsers.map((pu) => { + return ( +
+
+
+ {pu.name} + ({pu.username}) + + 待审申请 + +
+
+ 部门: {pu.department || '未填写'} + 工号: {pu.employeeId || '未填写'} + 申请时间: {pu.createdAt || '近期'} +
+
+ +
+
+ 指派职位: + +
+ + + + +
+
+ ); + })} +
+ )} +
+ )} + + {/* 3. 系统准入策略 */} + {activeTab === 'policy' && ( +
+
+

系统账号准入与注册安全策略

+

+ 控制外部人员能否自行在登录界面注册,以及新用户是否必须经过管理员审批与受控职位授权 +

+
+ +
+ {/* 开关 1: 开放自主注册 */} +
+
+
+ 允许员工自主注册账号 + + {systemConfig.allowRegistration ? '已开启' : '已暂停'} + +
+

+ 关闭后,登录主页将彻底隐藏注册入口,仅允许系统管理员在后台手动添加开通账号。 +

+
+ + +
+ + {/* 开关 2: 注册需审批 */} +
+
+
+ 新注册账号必须经过管理员审批 + + {systemConfig.requireApproval ? '强制审批' : '免审激活'} + +
+

+ 开启后,自主注册的账号处于待审核状态无法登录,必须由管理员在“新用户注册审核”中心指派职位并激活。 +

+
+ + +
+
+
+ )} + + {/* 4. 全局角色与权限矩阵 (RBAC) */} + {activeTab === 'rbac' && ( +
+
+
+

全局动态角色与 7 项原子权限矩阵

+

+ 支持动态增删角色,定制各职位默认拥有的全局业务权限能力 +

+
+
+ + +
+
+ + {/* 角色卡片选择 */} +
+ {editRoles.map((r) => { + const isSelected = r.id === selectedRoleId; + return ( +
setSelectedRoleId(r.id)} + className={`p-3 rounded-xl border cursor-pointer transition ${ + isSelected + ? 'bg-amber-500/10 border-amber-500/50 text-white shadow-sm' + : 'bg-slate-950/60 border-slate-800 text-slate-400 hover:border-slate-700' + }`} + > +
+ {r.name} + {r.isSystem && ( + + 内置 + + )} +
+
{r.code}
+

{r.description}

+
+ ); + })} +
+ + {/* 选定角色的权限勾选矩阵 */} + {selectedRole && ( +
+
+
+ + 配置【{selectedRole.name}】的授权项 + + + (已选 {selectedRole.permissions.length} 项) + +
+ {selectedRole.isSystem && selectedRole.code === 'ADMIN' && ( + + 系统管理员拥有全部底层通配权限 + + )} +
+ +
+ {ALL_PERMISSION_ITEMS.map((perm) => { + const checked = + selectedRole.code === 'ADMIN' || selectedRole.permissions.includes(perm.key); + const disabled = selectedRole.code === 'ADMIN'; + + return ( + + ); + })} +
+
+ )} +
+ )} + + {/* 5. 环境与运行状态 */} + {activeTab === 'system' && ( +
+
+

系统运行架构与环境隔离状态

+

+ 监控开发、测试与生产环境三套隔离状态,以及知识库共享与大模型后端运行健康度 +

+
+ +
+
+ + + 运行环境 (ENV) + +
{env.toUpperCase()}
+

+ {env === 'dev' + ? '开发环境:支持测试人员调试快捷身份切换,数据隔离在 data/dev' + : '测试/生产环境:严格禁用身份切换,仅允许真实登出与登录'} +

+
+ +
+ + + 超级管理员凭据 (.env) + +
+ ADMIN_USERNAME: admin +
+
+ ADMIN_PASSWORD: •••••••••••••• +
+

+ 系统管理员凭据严格从运行环境环境变量动态注入,具备防死锁兜底 +

+
+
+ +
+ 跨环境隔离合规准则摘要 +
+

• 知识库共享:共享合规法规数据库(端口 5432,数据库 regulations)。

+

• 对话与业务库隔离(红线):各环境对话历史、草稿与受控文档库物理隔离在各自目录,互不可见。

+

• 身份切换安全隔离(红线):真实测试(test)与发布(main)环境禁止一切快捷身份切换,必须凭密码登录。

+
+
+
+ )} +
+
+
+ + {/* 新建员工账号弹窗 */} + {showCreateModal && ( +
+
+
+

+ + 管理员开通新员工账号 +

+ +
+ +
+
+
+ + setNewName(e.target.value)} + placeholder="例如: 赵工" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 focus:outline-none focus:border-amber-500" + /> +
+
+ + setNewUsername(e.target.value)} + placeholder="如: zhaogong" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 focus:outline-none focus:border-amber-500" + /> +
+
+ +
+ + setNewInitialPassword(e.target.value)} + placeholder="默认 123456" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 focus:outline-none focus:border-amber-500 font-mono" + /> +
+ +
+ + +
+ +
+
+ + setNewDepartment(e.target.value)} + placeholder="如: 研发中心" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-200" + /> +
+
+ + setNewEmployeeId(e.target.value)} + placeholder="如: RD-099" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-200" + /> +
+
+ +
+ + 核心约束:由管理员创建的账号,首次登录系统时将被强制要求修改密码。 +
+ +
+ + +
+
+
+
+ )} + + {/* 新增自定义角色对话框 */} + {showNewRoleDialog && ( +
+
+

新增自定义受控角色

+
+
+ + setNewRoleName(e.target.value)} + placeholder="如: 合规审计专家" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100" + /> +
+
+ + setNewRoleCode(e.target.value.toUpperCase())} + placeholder="如: AUDITOR" + className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-100 font-mono" + /> +
+
+ +