Merge remote-tracking branch 'origin/main' into feature/rdm-two-trees-readable-audit

This commit is contained in:
2026-10-15 06:05:19 +00:00
2 changed files with 70 additions and 0 deletions
+49
View File
@@ -278,6 +278,18 @@ interface DutyCatalogDoc {
assigned: boolean; author?: string; reviewers?: string[]; approver?: string; activeRound?: number
}
interface DocsTemplateRow {
templateId: string; version: string; title: string; docTypes: string[]; stage?: string
}
/** rdm.docs.instantiate 参数:docType 取首个适用类型;空标题交由后端取模板标题。 */
export function docsInstantiateParams(tpl: DocsTemplateRow, title: string): Record<string, unknown> {
return {
docType: tpl.docTypes[0], templateId: tpl.templateId, version: tpl.version,
...(title.trim() ? { title: title.trim() } : {}),
}
}
interface DutyGetOut {
docId: string; title: string; assigned: boolean
roles: { author: string; reviewers: string[]; approver: string }
@@ -320,6 +332,9 @@ export function RdmDutiesSection({ project }: { project: Project }) {
const [err, setErr] = useState('')
const [notice, setNotice] = useState('')
const [busy, setBusy] = useState(false)
const [tpls, setTpls] = useState<DocsTemplateRow[] | null>(null)
const [tplSel, setTplSel] = useState('')
const [newTitle, setNewTitle] = useState('')
const cmd = <T,>(id: string, params: Record<string, unknown>) => api.rdmProjectCommand<T>(project, id, params)
const memberPage = useMemberPickerPage(members)
@@ -388,6 +403,9 @@ export function RdmDutiesSection({ project }: { project: Project }) {
cmd<{ members: { owner: string; role: string }[] }>('rdm.project.member.list', { projectId })
.then(out => { if (currentProject(epoch)) setMembers(out.members ?? []) })
.catch(() => { if (currentProject(epoch)) setMembers([]) })
api.rdmCommand<DocsTemplateRow[]>('rdm.docs.templates')
.then(rows => { if (currentProject(epoch)) setTpls(rows ?? []) })
.catch(() => { if (currentProject(epoch)) setTpls([]) })
return () => {
requests.current.epoch++; requests.current.selection++; requests.current.status++
requests.current.catalog++; requests.current.action++; requests.current.selected = ''
@@ -431,8 +449,39 @@ export function RdmDutiesSection({ project }: { project: Project }) {
cmd('rdm.review.assign.doc.batch', { projectId, docs: [...batchSel].map(d => ({
docId: d, author: batchAuthor[0], reviewers: batchReviewers, approver: batchApprover[0] })) }))
/** U4/UI:按已发布 DHF 模板整份生成文档(章节骨架自动播种),随后选中新文档。 */
const instantiate = () => {
const tpl = (tpls ?? []).find(r => `${r.templateId}@${r.version}` === tplSel)
if (!tpl) return
const title = newTitle.trim()
return run(`已按模板生成「${title || tpl.title}」`, async () => {
const res = await cmd<{ docId: string }>('rdm.docs.instantiate', docsInstantiateParams(tpl, title))
await refreshCatalog()
await selectDoc(res.docId, true)
})
}
const tpl = (tpls ?? []).find(r => `${r.templateId}@${r.version}` === tplSel)
return (
<div>
<h4 className="settings-group-title">按模板生成文档</h4>
<div className="ad-create">
<div className="ad-row">
<select className="lg-input" aria-label="文档模板" value={tplSel}
onChange={e => { setTplSel(e.target.value); setNewTitle('') }}>
<option value="">{tpls === null ? '模板加载中…' : '选择模板…'}</option>
{(tpls ?? []).map(r => (
<option key={`${r.templateId}@${r.version}`} value={`${r.templateId}@${r.version}`}>
{r.title}({r.templateId}@{r.version})
</option>
))}
</select>
<input className="lg-input" aria-label="文档标题" placeholder={`文档标题(默认:${tpl?.title ?? '模板标题'})`}
value={newTitle} onChange={e => setNewTitle(e.target.value)} />
<button className="lg-submit ad-create-btn" disabled={busy || !tpl} onClick={instantiate}>生成文档</button>
</div>
<p className="ad-hint">按已发布 DHF 模板整份生成:章节骨架自动播种;生成后在下方清单选中文档分配三权并走整文审签。</p>
</div>
<h4 className="settings-group-title">文档清单(职责分配对象为整份文档;搜索/浏览同一目录)</h4>
<table className="ad-table">
<thead><tr><th></th><th>文档</th><th>类型 / 阶段</th><th>当前职责</th><th>状态</th><th></th></tr></thead>
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { docsInstantiateParams } from '../src/components/adminRdmSections'
const tpl = {
templateId: 'dhf.2750fbaf1f67d10c', version: '1.0.0', title: '项目建议书',
docTypes: ['rd_proposal'], stage: '0. 商务阶段',
}
describe('docsInstantiateParams', () => {
it('carries docType/templateId/version and trims the title', () => {
expect(docsInstantiateParams(tpl, ' U5 试点项目建议书 ')).toEqual({
docType: 'rd_proposal', templateId: 'dhf.2750fbaf1f67d10c',
version: '1.0.0', title: 'U5 试点项目建议书',
})
})
it('omits blank titles so the backend falls back to the template title', () => {
const p = docsInstantiateParams(tpl, ' ')
expect(Object.hasOwn(p, 'title')).toBe(false)
expect(p).toEqual({ docType: 'rd_proposal', templateId: 'dhf.2750fbaf1f67d10c', version: '1.0.0' })
})
})