diff --git a/src/components/ProjectSettingsModal.tsx b/src/components/ProjectSettingsModal.tsx index e1b0377..eae4004 100644 --- a/src/components/ProjectSettingsModal.tsx +++ b/src/components/ProjectSettingsModal.tsx @@ -7,7 +7,7 @@ import { store, useStore } from '../store' import type { SettingDesc } from '../contracts' import { SettingsShell, Btn, EmptyState } from '@ui' import { SettingInput, flatten, unflatten, groupNames } from './SettingsModal' -import { RdmMembersSection, RdmDutiesSection } from './adminRdmSections' +import { RdmMembersSection, RdmDutiesSection, RdmPermMatrixSection } from './adminRdmSections' export function ProjectSettingsModal() { const projectId = useStore(s => s.ui.projectSettingsId) @@ -15,6 +15,7 @@ export function ProjectSettingsModal() { const project = projects.find(p => p.id === projectId) const rdmMembersAvailable = useStore(s => s.rdmMembersAvailable) const rdmDocReviewAvailable = useStore(s => s.rdmDocReviewAvailable) + const rdmPermMatrixAvailable = useStore(s => s.rdmPermMatrixAvailable) const [tab, setTab] = useState('project') const [schema, setSchema] = useState([]) const [draft, setDraft] = useState>({}) @@ -61,15 +62,17 @@ export function ProjectSettingsModal() { // U6/KTD5:成员与文档职责页签按 RDM 命令面门控(普通基座不出现)。 ...(rdmMembersAvailable ? [{ key: 'members', label: '成员', icon: 'users' as const }] : []), ...(rdmDocReviewAvailable ? [{ key: 'duties', label: '文档职责 · 整文审签', icon: 'shield' as const }] : []), + ...(rdmPermMatrixAvailable ? [{ key: 'matrix', label: '项目权限', icon: 'key' as const }] : []), ]} footer={saved ? ✓ 已保存 : '项目设置覆盖用户设置(new-run 类对新任务生效)'} >
{tab === 'members' && } {tab === 'duties' && } - {tab !== 'members' && tab !== 'duties' && !loaded ? ( + {tab === 'matrix' && } + {tab !== 'members' && tab !== 'duties' && tab !== 'matrix' && !loaded ? ( 加载中… - ) : tab !== 'members' && tab !== 'duties' ? ( + ) : tab !== 'members' && tab !== 'duties' && tab !== 'matrix' ? ( <>
此处仅保存本项目的覆盖值;留空即沿用用户/系统设置。跨会话读取在「会话协作」组。 diff --git a/src/components/adminRdmSections.tsx b/src/components/adminRdmSections.tsx index 9d7d443..5c823fb 100644 --- a/src/components/adminRdmSections.tsx +++ b/src/components/adminRdmSections.tsx @@ -12,7 +12,7 @@ import { roleCreatePayload, roleActionPayload, roleAssignPayload, roleDisablePayload, memberAddPayload, memberRemovePayload, assignDocDutiesPayload, docSubmitPayload, docApprovePayload, docRejectPayload, - docFinalPayload, docStatusPayload, + docFinalPayload, docStatusPayload, permCheckPayload, describeSource, } from '../rdmManagement' // ---- 系统管理:RDM 角色权限(仅管理员 + rdmRolesAvailable) ---- @@ -307,3 +307,79 @@ export function RdmDutiesSection({ projectId }: { projectId: string }) {
) } + + +// ---- 项目设置:项目权限(矩阵解释读面,与实际判定同源——UI 不重算) ---- + +interface MatrixAction { action: string; allowed: boolean; source: string; reason?: string } +interface MatrixOut { + projectId: string; member: string; role: string + profRole: { roleId: string; name: string } | null + actions: MatrixAction[] +} + +export function RdmPermMatrixSection({ projectId }: { projectId: string }) { + const me = useStore(s => s.me) + const [member, setMember] = useState('') + const [members, setMembers] = useState(null) + const [matrix, setMatrix] = useState(null) + const [err, setErr] = useState('') + const [busy, setBusy] = useState(false) + + useEffect(() => { + api.rdmCommand<{ members: MemberRow[] }>('rdm.project.member.list', { projectId }) + .then(out => { + setMembers(out.members ?? []) + // 默认自省:普通成员只看自己的矩阵;负责人/管理员可切换他人。 + if (me?.owner) setMember(prev => prev || me.owner) + }) + .catch(e => setErr(String(e))) + }, [projectId, me?.owner]) + + const query = async (m: string) => { + if (!m) return + setErr(''); setBusy(true); setMatrix(null) + try { + setMatrix(await api.rdmCommand('rdm.perm.check', permCheckPayload(projectId, m))) + } catch (e) { setErr(String(e)) } finally { setBusy(false) } + } + + return ( +
+
+
+ + query(member)}>查看矩阵 +
+

普通成员可查看自己的有效权限;查看他人矩阵须项目管理权。矩阵与实际判定同源(服务端解释,非前端推算)。

+
+ {matrix && ( + <> +
+ 成员 {matrix.member} · 项目关系 {matrix.role} + {matrix.profRole && <> · 专业角色 {matrix.profRole.name}({matrix.profRole.roleId})} + +
+ + + + {(matrix.actions ?? []).map(a => ( + + + + + + + ))} + +
动作生效来源原因(拒绝时)
{a.action}{a.allowed ? 允许 : 拒绝}{describeSource(a.source)}{a.reason || '—'}
+ + )} + {!matrix && !err && members !== null &&

选择成员后展示 12 项动作的有效权限与第一拒绝原因。

} + {err &&
{err}
} +
+ ) +} diff --git a/src/rdmManagement.ts b/src/rdmManagement.ts index 9a9eb3d..5635cce 100644 --- a/src/rdmManagement.ts +++ b/src/rdmManagement.ts @@ -19,6 +19,12 @@ export const RDM_MEMBER_COMMANDS = [ 'rdm.project.member.remove', ] as const +/** 项目设置「项目权限」页消费的命令面(矩阵解释 + 成员清单)。 */ +export const RDM_PERM_MATRIX_COMMANDS = [ + 'rdm.perm.check', + 'rdm.project.member.list', +] as const + /** 项目设置「文档职责 · 整文审签」页消费的命令面。 */ export const RDM_DOC_REVIEW_COMMANDS = [ 'rdm.review.assign.doc', @@ -48,6 +54,10 @@ export const rdmMembersReady = (cmds: CommandListItem[] | null | undefined) => export const rdmDocReviewReady = (cmds: CommandListItem[] | null | undefined) => ready(cmds, RDM_DOC_REVIEW_COMMANDS) +/** 项目权限矩阵页是否就绪。 */ +export const rdmPermMatrixReady = (cmds: CommandListItem[] | null | undefined) => + ready(cmds, RDM_PERM_MATRIX_COMMANDS) + /** RDM 动作清单(12 项,与 RDM perms.AllActions 对齐;角色动作编辑器选项)。 */ export const ROLE_ACTIONS = [ 'project.create', 'project.archive', 'member.manage', 'duty.assign', @@ -81,6 +91,20 @@ export const docFinalPayload = (projectId: string, docId: string, round = 0) => round > 0 ? { projectId, docId, round } : { projectId, docId } export const docStatusPayload = (projectId: string, docId: string) => ({ projectId, docId }) +export const permCheckPayload = (projectId: string, member: string) => ({ projectId, member }) + +/** 矩阵来源标签(人话映射;未知来源回退原文——服务端新增来源时不至于空白)。 */ +export function describeSource(source: string): string { + switch (source) { + case 'admin': return '系统管理员' + case 'prof-role-default': return '专业角色默认' + case 'override': return '项目内覆盖' + case 'relation-default': return '项目关系默认' + case 'non-member': return '非项目成员' + default: return source + } +} + /** 审阅人 CSV 解析:去空白、去空项、保序去重(表单输入 → reviewers 数组)。 */ export function parseReviewers(csv: string): string[] { const seen = new Set() diff --git a/src/store.ts b/src/store.ts index 53a07ee..c99f0d9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -9,7 +9,7 @@ import { canFlushOutbox, deriveShellPhase } from './runstate' import { flashElement } from './scrollsync' import { isThemeChoice, resolveTheme, type ThemeChoice } from './theme' import { rdmAuditReady } from './workspaces' -import { rdmRolesReady, rdmMembersReady, rdmDocReviewReady } from './rdmManagement' +import { rdmRolesReady, rdmMembersReady, rdmDocReviewReady, rdmPermMatrixReady } from './rdmManagement' type FontSizeChoice = 13 | 14 | 16 | 18 const FONT_SIZE_CHOICES: readonly FontSizeChoice[] = [13, 14, 16, 18] @@ -120,6 +120,7 @@ interface AppState { rdmRolesAvailable: boolean rdmMembersAvailable: boolean rdmDocReviewAvailable: boolean + rdmPermMatrixAvailable: boolean /** 管理区声明分区(WP3-I3):编译插件声明,入口全员可见、区内置权限不合并。 */ adminSections: AdminZoneSection[] ui: UIState @@ -186,7 +187,7 @@ let state: AppState = { plan: null, todos: null, artifacts: [], surfaces: [], projectSurfaces: [], cards: [], projectCards: [], inboxScope: 'conv', runtimeMock: false, profiles: [], pluginPanels: [], serverWorkspaces: [], rdmAuditAvailable: false, - rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, + rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, adminSections: [], ui: loadUI(), connected: false, posting: false, error: null, toast: null, outbox: [], outboxDraining: false, @@ -805,6 +806,7 @@ export const store = { rdmRolesAvailable: rdmRolesReady(cmds), rdmMembersAvailable: rdmMembersReady(cmds), rdmDocReviewAvailable: rdmDocReviewReady(cmds), + rdmPermMatrixAvailable: rdmPermMatrixReady(cmds), }) } catch { set({ rdmAuditAvailable: false }) } try {