Why: 方案 r1 U6/R4/KTD5——RDM #266 已交付与实际判定同源的矩阵解释读面,项目设置需消费入口(UI 不重算,服务端解释)。 Evidence: 红:3 failed(导出缺失,/tmp/fe-matrix-red.txt);绿:vitest 25 文件/127 测试全过(含 design-guard)+ npm run build 成功(tsc -b 干净);收据 /tmp/fe-matrix-vitest.txt、/tmp/fe-matrix-build.txt。 Impact: rdmManagement 增 RDM_PERM_MATRIX_COMMANDS/rdmPermMatrixReady/permCheckPayload/describeSource;RdmPermMatrixSection(成员选择默认自省+12 动作矩阵表+来源标签+拒绝原因);项目设置「项目权限」页签(rdmPermMatrixAvailable 门控);store 同源计算标志。样式仅复用既有类。 Refs: RDM #266;方案 r1 U6/R4
101 lines
4.5 KiB
TypeScript
101 lines
4.5 KiB
TypeScript
// 项目设置面板(提案 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'
|
||
|
||
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 [tab, setTab] = useState('project')
|
||
const [schema, setSchema] = useState<SettingDesc[]>([])
|
||
const [draft, setDraft] = useState<Record<string, any>>({})
|
||
const [loaded, setLoaded] = useState(false)
|
||
const [saved, setSaved] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (!projectId) return
|
||
setLoaded(false)
|
||
api.getSettings(projectId).then(s => {
|
||
setSchema(s.schema)
|
||
setDraft(flatten(s.project))
|
||
setLoaded(true)
|
||
}).catch(() => undefined)
|
||
}, [projectId])
|
||
|
||
const groups = useMemo(() => {
|
||
const g: Record<string, SettingDesc[]> = {}
|
||
for (const d of schema) (g[d.group] ??= []).push(d)
|
||
return g
|
||
}, [schema])
|
||
|
||
if (!projectId) return null
|
||
|
||
const save = async () => {
|
||
if (!projectId) return
|
||
try {
|
||
await api.putProjectSettings(projectId, unflatten(draft))
|
||
setSaved(true)
|
||
setTimeout(() => setSaved(false), 1500)
|
||
} catch (e) {
|
||
store.toast(`项目设置保存失败:${e}`)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<SettingsShell
|
||
title={`项目设置 · ${project?.name ?? projectId}`}
|
||
active={tab}
|
||
onSelect={setTab}
|
||
onClose={() => 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 ? <span style={{ color: 'var(--ok)' }}>✓ 已保存</span> : '项目设置覆盖用户设置(new-run 类对新任务生效)'}
|
||
>
|
||
<div>
|
||
{tab === 'members' && <RdmMembersSection projectId={projectId} />}
|
||
{tab === 'duties' && <RdmDutiesSection projectId={projectId} />}
|
||
{tab === 'matrix' && <RdmPermMatrixSection projectId={projectId} />}
|
||
{tab !== 'members' && tab !== 'duties' && tab !== 'matrix' && !loaded ? (
|
||
<EmptyState icon="settings">加载中…</EmptyState>
|
||
) : tab !== 'members' && tab !== 'duties' && tab !== 'matrix' ? (
|
||
<>
|
||
<div className="empty-note" style={{ marginBottom: 10 }}>
|
||
此处仅保存本项目的覆盖值;留空即沿用用户/系统设置。跨会话读取在「会话协作」组。
|
||
</div>
|
||
{Object.entries(groups).map(([g, descs]) => (
|
||
<div className="settings-group" key={g}>
|
||
<h4>{groupNames[g] ?? g}</h4>
|
||
{descs.map(d => (
|
||
<div className="setting-row" key={d.key}>
|
||
<label title={d.key}>{d.label}</label>
|
||
<SettingInput desc={d} value={draft[d.key] ?? ''} onChange={v => setDraft(x => ({ ...x, [d.key]: v }))} />
|
||
<span className={`effect-badge effect-${d.effect}`} title={`生效时机:${d.effect}`}>
|
||
{d.effect === 'hot' ? '立即' : d.effect === 'new-run' ? '新任务' : '需重启'}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
<Btn variant="primary" icon="check" onClick={save}>保存项目设置</Btn>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
</SettingsShell>
|
||
)
|
||
}
|