// 项目设置面板(提案 2026-09-30-cross-session-reading):首个项目级设置 UI。 // schema 驱动渲染 project scope(4.9 覆盖链:system → user → project), // 写 putProjectSettings;后续项目级设置项经 settings schema 自动出现,零开发。 import { useEffect, useMemo, useState } from 'react' import { api } from '../api' 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, RdmPermMatrixSection } from './adminRdmSections' import { rdmProjectReadError } from '../rdmManagement' import { projectActionError, projectWorkError } from '../projectmeta' export function ProjectSettingsModal() { const projectId = useStore(s => s.ui.projectSettingsId) const projects = useStore(s => s.projects) 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 owner = useStore(s => s.me?.owner) const [tab, setTab] = useState('project') const [schema, setSchema] = useState([]) const [draft, setDraft] = useState>({}) const [loaded, setLoaded] = useState(false) const [saved, setSaved] = useState(false) const [error, setError] = useState('') useEffect(() => { if (!projectId) return let active = true setLoaded(false); setSaved(false); setError(''); setDraft({}); setSchema([]) api.getSettings(projectId).then(s => { if (!active) return setSchema(s.schema) setDraft(flatten(s.project)) setLoaded(true) }).catch(() => { if (active) setError('项目设置未能加载,请关闭后重新打开或联系项目负责人') }) return () => { active = false } }, [projectId, owner]) const groups = useMemo(() => { const g: Record = {} for (const d of schema) (g[d.group] ??= []).push(d) return g }, [schema]) if (!projectId) return null const rdmError = rdmProjectReadError(project) const writeError = projectWorkError(project) const rdmTab = tab === 'members' || tab === 'duties' || tab === 'matrix' const save = async () => { if (!projectId) return if (writeError) { store.toast(writeError); return } try { await api.putProjectSettings(projectId, unflatten(draft)) setSaved(true) setTimeout(() => setSaved(false), 1500) } catch (e) { store.toast(`项目设置保存失败:${projectActionError(e)}`) } } return ( store.openProjectSettings(null)} sections={[ { key: 'project', label: '本项目', icon: 'folder' }, // 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 类对新任务生效)'} >
{rdmTab && rdmError ? {rdmError} : project && <> {tab === 'members' && } {tab === 'duties' && } {tab === 'matrix' && } } {tab !== 'members' && tab !== 'duties' && tab !== 'matrix' && !loaded ? ( {error || '加载中…'} ) : tab !== 'members' && tab !== 'duties' && tab !== 'matrix' ? ( <>
{writeError ?? '此处仅保存本项目的覆盖值;留空即沿用用户/系统设置。跨会话读取在「会话协作」组。'}
{Object.entries(groups).map(([g, descs]) => (

{groupNames[g] ?? g}

{descs.map(d => (
setDraft(x => ({ ...x, [d.key]: v }))} /> {d.effect === 'hot' ? '立即' : d.effect === 'new-run' ? '新任务' : '需重启'}
))}
))} 保存项目设置 ) : null}
) }