@@ -0,0 +1,467 @@
// U4:项目追溯审计工作区(四入口)——消费 U3 rdm.inspect.* 命令面。
// 只读:审批/确认关系仍回原业务流程(D31:弹层不是唯一路径)。
// 令牌驱动(DESIGN.md):颜色/字号/圆角仅消费 @ui/tokens.css;折叠用 Collapsible。
import { useCallback , useEffect , useRef , useState } from 'react'
import { api } from '../api'
import { Chip , EmptyState , SectionLabel } from '@ui'
// ---- 类型(U3 ListShell/详情的精简前端契约) ----
interface InspectProject { projectId : string ; name : string ; canAudit : boolean }
interface Capabilities { actor : string ; projects : InspectProject [ ] ; crossProject : boolean }
interface CoverageItem { face : string ; reason : string }
interface ListShell < T = any > { items : T [ ] ; nextCursor? : string ; hasMore : boolean ; truncated : boolean ; coverage? : CoverageItem [ ] }
interface SummaryData { projectId : string ; counts : Record < string , number > ; watermark : number ; lastOpAt : string ; coverage? : CoverageItem [ ] }
interface TreeItem { kind : string ; id : string ; title : string ; state? : string }
interface RelationEntry { layer : string ; edgeId? : string ; kind? : string ; type ? : string ; state : string ; depth? : number ; from : any ; to : any ; conf? : number }
interface EventDTO { seq : number ; at : string ; op : string ; actor : string ; source : string ; target : { kind : string ; id : string } ; detail : string ; operationId : string ; disclosure : string }
interface OpGroup { operationId : string ; actor : string ; at : string ; events : EventDTO [ ] }
interface ObjectDetail { kind : string ; id : string ; title? : string ; state? : string ; versions? : any [ ] ; formal? : any ; labels? : any ; batch? : string ; ver? : number ; relations : RelationEntry [ ] ; recentEvents : EventDTO [ ] ; coverage? : CoverageItem [ ] }
interface HistoryData { projectId : string ; watermark : number ; atSeq : number ; object ? : any ; formal? : any ; formalMissing? : string ; countOnlySnapshots? : number ; coverage? : CoverageItem [ ] ; empty? : boolean }
// ---- 工作区主组件 ----
const TABS = [
{ id : 'summary' , label : '概览' } ,
{ id : 'materials' , label : '资料与文档' } ,
{ id : 'trace' , label : '关联追溯' } ,
{ id : 'timeline' , label : '操作记录' } ,
] as const
type TabId = ( typeof TABS ) [ number ] [ 'id' ]
export function RdmAuditWorkspace() {
const [ caps , setCaps ] = useState < Capabilities | null > ( null )
const [ pid , setPid ] = useState ( '' )
const [ tab , setTab ] = useState < TabId > ( 'summary' )
const [ error , setError ] = useState ( '' )
useEffect ( ( ) = > {
let cancelled = false
api . rdmCommand < Capabilities > ( 'rdm.inspect.capabilities' )
. then ( c = > { if ( ! cancelled ) { setCaps ( c ) ; if ( c . projects . length > 0 ) setPid ( prev = > prev || c . projects [ 0 ] . projectId ) } } )
. catch ( e = > { if ( ! cancelled ) setError ( e . message ) } )
return ( ) = > { cancelled = true }
} , [ ] )
const project = caps ? . projects . find ( p = > p . projectId === pid )
if ( error ) return < EmptyState icon = "alert" > { error } < / EmptyState >
if ( ! caps ) return < div className = "empty-note" > 加 载 中 … < / div >
if ( caps . projects . length === 0 ) return < EmptyState icon = "folder" > 无 可 见 研 发 项 目 — — 项 目 创 建 者 与 域 管 理 员 可 查 看 追 溯 审 计 < / EmptyState >
return (
< div className = "rdm-audit" >
< div className = "rdm-audit-header" >
< select className = "ui-input rdm-audit-project" value = { pid } onChange = { e = > setPid ( e . target . value ) } aria-label = "选择项目" >
{ caps . projects . map ( p = > (
< option key = { p . projectId } value = { p . projectId } > { p . name } < / option >
) ) }
< / select >
{ project ? . canAudit && < Chip tone = "ok" > 可 审 计 < / Chip > }
{ caps . crossProject && < Chip tone = "info" > 跨 项 目 < / Chip > }
< / div >
< div className = "tabs rdm-audit-tabs" role = "tablist" >
{ TABS . map ( t = > (
< button key = { t . id } role = "tab" aria-selected = { tab === t . id }
className = { ` tab ${ tab === t . id ? ' active' : '' } ` }
onClick = { ( ) = > setTab ( t . id ) } > { t . label } < / button >
) ) }
< / div >
< div className = "rdm-audit-body" >
{ tab === 'summary' && < SummaryTab pid = { pid } / > }
{ tab === 'materials' && < MaterialsTab pid = { pid } / > }
{ tab === 'trace' && < TraceTab pid = { pid } / > }
{ tab === 'timeline' && < TimelineTab pid = { pid } / > }
< / div >
< / div >
)
}
// ---- 概览 ----
function SummaryTab ( { pid } : { pid : string } ) {
const [ data , setData ] = useState < SummaryData | null > ( null )
const [ err , setErr ] = useState ( '' )
useEffect ( ( ) = > {
let c = false
setData ( null ) ; setErr ( '' )
api . rdmCommand < SummaryData > ( 'rdm.inspect.summary' , { projectId : pid } )
. then ( d = > { if ( ! c ) setData ( d ) } )
. catch ( e = > { if ( ! c ) setErr ( e . message ) } )
return ( ) = > { c = true }
} , [ pid ] )
if ( err ) return < EmptyState icon = "alert" > { err } < / EmptyState >
if ( ! data ) return < div className = "empty-note" > 加 载 中 … < / div >
const counts = data . counts || { }
const entries : [ string , string , number ] [ ] = [
[ '切片' , 'slices' , counts . slices ? ? 0 ] ,
[ '章节' , 'sections' , counts . sections ? ? 0 ] ,
[ '文档' , 'docs' , counts . docs ? ? 0 ] ,
[ '生效边' , 'edgesActive' , counts . edgesActive ? ? 0 ] ,
[ '待确认边' , 'edgesCandidate' , counts . edgesCandidate ? ? 0 ] ,
[ '结论' , 'conclusions' , counts . conclusions ? ? 0 ] ,
[ '待办' , 'openItems' , counts . openItems ? ? 0 ] ,
[ '挂起上传' , 'uploadsHeld' , counts . uploadsHeld ? ? 0 ] ,
[ '空缺章节' , 'emptySections' , counts . emptySections ? ? 0 ] ,
]
return (
< div >
< SectionLabel > 统 计 ( 截 至 水 位 { data . watermark } ) < / SectionLabel >
< div className = "rdm-audit-cards" >
{ entries . map ( ( [ label , , val ] ) = > (
< div key = { label } className = "rdm-audit-card" >
< div className = "rdm-audit-card-num" > { val } < / div >
< div className = "rdm-audit-card-label" > { label } < / div >
< / div >
) ) }
< / div >
{ data . lastOpAt && < div className = "rdm-audit-meta" > 最 近 动 作 : { new Date ( data . lastOpAt ) . toLocaleString ( 'zh-CN' ) } < / div > }
{ data . coverage && data . coverage . length > 0 && (
< CoverageList cov = { data . coverage } / >
) }
< / div >
)
}
// ---- 资料与文档 ----
function MaterialsTab ( { pid } : { pid : string } ) {
const [ kind , setKind ] = useState < 'notes' | 'docs' > ( 'docs' )
return (
< div className = "rdm-audit-materials" >
< div className = "rdm-audit-materials-side" >
< div className = "tabs" >
< button className = { ` tab ${ kind === 'notes' ? ' active' : '' } ` } onClick = { ( ) = > setKind ( 'notes' ) } > 笔 记 树 < / button >
< button className = { ` tab ${ kind === 'docs' ? ' active' : '' } ` } onClick = { ( ) = > setKind ( 'docs' ) } > 文 档 树 < / button >
< / div >
< TreeList pid = { pid } kind = { kind } / >
< / div >
< / div >
)
}
function TreeList ( { pid , kind } : { pid : string ; kind : 'notes' | 'docs' } ) {
const [ shell , setShell ] = useState < ListShell < TreeItem > | null > ( null )
const [ err , setErr ] = useState ( '' )
const [ selected , setSelected ] = useState < TreeItem | null > ( null )
const [ detail , setDetail ] = useState < ObjectDetail | null > ( null )
const genRef = useRef ( 0 )
useEffect ( ( ) = > {
const gen = ++ genRef . current
setShell ( null ) ; setErr ( '' ) ; setSelected ( null ) ; setDetail ( null )
api . rdmCommand < ListShell < TreeItem > > ( 'rdm.inspect.tree' , { projectId : pid , kind , cursor : '' } )
. then ( s = > { if ( gen === genRef . current ) setShell ( s ) } )
. catch ( e = > { if ( gen === genRef . current ) setErr ( e . message ) } )
} , [ pid , kind ] )
const loadMore = ( ) = > {
if ( ! shell ? . nextCursor ) return
const gen = genRef . current
api . rdmCommand < ListShell < TreeItem > > ( 'rdm.inspect.tree' , { projectId : pid , kind , cursor : shell.nextCursor } )
. then ( s = > { if ( gen === genRef . current ) setShell ( prev = > prev ? { . . . s , items : [ . . . prev . items , . . . s . items ] } : s ) } )
. catch ( e = > { if ( gen === genRef . current ) setErr ( e . message ) } )
}
const selectNode = ( item : TreeItem ) = > {
setSelected ( item ) ; setDetail ( null )
const gen = genRef . current
api . rdmCommand < ObjectDetail > ( 'rdm.inspect.object' , { projectId : pid , kind : item.kind , id : item.id } )
. then ( d = > { if ( gen === genRef . current ) setDetail ( d ) } )
. catch ( e = > { if ( gen === genRef . current ) setErr ( e . message ) } )
}
if ( err ) return < EmptyState icon = "alert" > { err } < / EmptyState >
if ( ! shell ) return < div className = "empty-note" > 加 载 中 … < / div >
if ( shell . items . length === 0 ) return < EmptyState icon = "folder" > 暂 无 { kind === 'notes' ? '骨架节点' : '文档' } < / EmptyState >
return (
< div className = "rdm-audit-tree" >
< div className = "rdm-audit-tree-list" >
{ shell . items . map ( item = > (
< button key = { item . id } className = { ` rdm-audit-tree-node ${ selected ? . id === item . id ? ' sel' : '' } ` }
onClick = { ( ) = > selectNode ( item ) } >
< span className = "rdm-audit-tree-kind" > { item . kind } < / span >
< span className = "rdm-audit-tree-title" > { item . title } < / span >
{ item . state && < Chip tone = { item . state === 'baselined' ? 'ok' : 'plain' } > { item . state } < / Chip > }
< / button >
) ) }
{ shell . hasMore && < button className = "btn v-ghost rdm-audit-more" onClick = { loadMore } > 加 载 更 多 < / button > }
{ shell . truncated && < div className = "rdm-audit-truncated" > 已 截 断 ( 深 度 或 数 量 上 限 ) < / div > }
< / div >
{ selected && (
< div className = "rdm-audit-detail" >
< SectionLabel > 对 象 详 情 < / SectionLabel >
{ ! detail ? < div className = "empty-note" > 加 载 中 … < / div > : < ObjectDetailView detail = { detail } / > }
< / div >
) }
< / div >
)
}
// ---- 关联追溯 ----
function TraceTab ( { pid } : { pid : string } ) {
const [ sel , setSel ] = useState < { kind : string ; id : string ; title : string } | null > ( null )
const [ relations , setRelations ] = useState < RelationEntry [ ] | null > ( null )
const [ truncated , setTruncated ] = useState ( false )
const [ err , setErr ] = useState ( '' )
const genRef = useRef ( 0 )
useEffect ( ( ) = > {
if ( ! sel ) return
const gen = ++ genRef . current
setRelations ( null ) ; setErr ( '' )
api . rdmCommand < ListShell < RelationEntry > > ( 'rdm.inspect.relations' , { projectId : pid , kind : sel.kind , id : sel.id , depth : 1 } )
. then ( s = > { if ( gen === genRef . current ) { setRelations ( s . items ) ; setTruncated ( s . truncated ) } } )
. catch ( e = > { if ( gen === genRef . current ) setErr ( e . message ) } )
} , [ pid , sel ] )
// 未选对象时列出章节供选择
const [ sections , setSections ] = useState < TreeItem [ ] > ( [ ] )
useEffect ( ( ) = > {
if ( sel ) return
let c = false
api . rdmCommand < ListShell < TreeItem > > ( 'rdm.inspect.tree' , { projectId : pid , kind : 'docs' } )
. then ( s = > { if ( ! c ) setSections ( s . items . filter ( i = > i . kind === 'section' ) ) } )
. catch ( ( ) = > { } )
return ( ) = > { c = true }
} , [ pid , sel ] )
if ( ! sel ) {
return (
< div >
< SectionLabel > 选 择 对 象 查 看 来 源 与 影 响 < / SectionLabel >
{ sections . length === 0 ? < EmptyState icon = "folder" > 暂 无 章 节 < / EmptyState > : (
< div className = "rdm-audit-pick" >
{ sections . map ( s = > (
< button key = { s . id } className = "btn v-ghost" onClick = { ( ) = > setSel ( { kind : 'section' , id : s.id , title : s.title } ) } >
{ s . title }
< / button >
) ) }
< / div >
) }
< / div >
)
}
const sources = relations ? . filter ( r = > r . to ? . id === sel . id && r . to ? . kind === sel . kind ) ? ? [ ]
const impacts = relations ? . filter ( r = > r . from ? . id === sel . id && r . from ? . kind === sel . kind ) ? ? [ ]
return (
< div >
< div className = "rdm-audit-trace-head" >
< button className = "btn v-ghost" onClick = { ( ) = > setSel ( null ) } > ← 返 回 < / button >
< strong > { sel . title } < / strong >
< Chip tone = "plain" > { sel . kind } : { sel . id } < / Chip >
< / div >
{ err && < EmptyState icon = "alert" > { err } < / EmptyState > }
{ ! relations && ! err && < div className = "empty-note" > 加 载 中 … < / div > }
{ relations && relations . length === 0 && < EmptyState icon = "folder" > 没 有 已 知 关 联 < / EmptyState > }
{ relations && relations . length > 0 && (
< div className = "rdm-audit-trace-cols" >
< div className = "rdm-audit-trace-col" >
< SectionLabel > 来 源 依 据 ( { sources . length } ) < / SectionLabel >
{ sources . length === 0 ? < div className = "empty-note" > 无 来 源 < / div > : sources . map ( ( r , i ) = > < RelationRow key = { i } r = { r } / > ) }
< / div >
< div className = "rdm-audit-trace-col" >
< SectionLabel > 影 响 对 象 ( { impacts . length } ) < / SectionLabel >
{ impacts . length === 0 ? < div className = "empty-note" > 无 影 响 < / div > : impacts . map ( ( r , i ) = > < RelationRow key = { i } r = { r } / > ) }
< / div >
< / div >
) }
{ truncated && < div className = "rdm-audit-truncated" > 已 截 断 ( 深 度 或 数 量 上 限 ) < / div > }
< / div >
)
}
function RelationRow ( { r } : { r : RelationEntry } ) {
const other = r . from ? . id && r . to ? . id ? ( r . from ? . id < r . to ? . id ? r.from : r.to ) : ( r . from || r . to )
return (
< div className = "rdm-audit-rel" >
< Chip tone = { r . layer === 'compliance' ? 'info' : 'plain' } > { r . layer } < / Chip >
< span className = "rdm-audit-rel-type" > { r . type || r . kind || '关联' } < / span >
< span className = "rdm-audit-rel-endpoint" > { other ? . kind } : { other ? . id } { other ? . ver ? ` @ ${ other . ver } ` : '' } < / span >
< Chip tone = { r . state === 'active' ? 'ok' : r . state === 'candidate' ? 'warn' : 'plain' } > { r . state } < / Chip >
< / div >
)
}
// ---- 操作记录(事件时间线) ----
function TimelineTab ( { pid } : { pid : string } ) {
const [ items , setItems ] = useState < ( OpGroup | EventDTO ) [ ] > ( [ ] )
const [ cursor , setCursor ] = useState ( '' )
const [ hasMore , setHasMore ] = useState ( false )
const [ err , setErr ] = useState ( '' )
const [ filterOp , setFilterOp ] = useState ( '' )
const [ filterActor , setFilterActor ] = useState ( '' )
const [ historyMode , setHistoryMode ] = useState ( false )
const [ historySeq , setHistorySeq ] = useState ( 0 )
const [ historyData , setHistoryData ] = useState < HistoryData | null > ( null )
const genRef = useRef ( 0 )
const load = useCallback ( ( cur : string , replace : boolean ) = > {
const gen = ++ genRef . current
if ( replace ) { setItems ( [ ] ) ; setErr ( '' ) }
const params : Record < string , unknown > = { projectId : pid , cursor : cur , limit : 30 }
if ( filterOp ) params . op = filterOp
if ( filterActor ) params . actor = filterActor
api . rdmCommand < ListShell < OpGroup | EventDTO > > ( 'rdm.inspect.events' , params )
. then ( s = > {
if ( gen !== genRef . current ) return
setItems ( prev = > replace ? s . items : [ . . . prev , . . . s . items ] )
setCursor ( s . nextCursor || '' )
setHasMore ( s . hasMore )
} )
. catch ( e = > { if ( gen === genRef . current ) setErr ( e . message ) } )
} , [ pid , filterOp , filterActor ] )
useEffect ( ( ) = > { if ( ! historyMode ) load ( '' , true ) } , [ load , historyMode ] )
useEffect ( ( ) = > {
if ( ! historyMode ) { setHistoryData ( null ) ; return }
const gen = genRef . current
setHistoryData ( null )
const params : Record < string , unknown > = { projectId : pid , atSeq : historySeq || undefined }
api . rdmCommand < HistoryData > ( 'rdm.inspect.history' , params )
. then ( d = > { if ( gen === genRef . current ) setHistoryData ( d ) } )
. catch ( e = > { if ( gen === genRef . current ) setErr ( e . message ) } )
} , [ pid , historyMode , historySeq ] )
return (
< div className = "rdm-audit-timeline" >
< div className = "rdm-audit-timeline-filters" >
< input className = "ui-input" placeholder = "过滤操作者…" value = { filterActor }
onChange = { e = > setFilterActor ( e . target . value ) } / >
< input className = "ui-input" placeholder = "过滤操作类型…" value = { filterOp }
onChange = { e = > setFilterOp ( e . target . value ) } / >
< button className = { ` btn v-ghost ${ historyMode ? ' active' : '' } ` } onClick = { ( ) = > setHistoryMode ( ! historyMode ) } >
{ historyMode ? '← 返回当前' : '历史模式' }
< / button >
{ historyMode && (
< input className = "ui-input rdm-audit-seq" type = "number" placeholder = "截止 seq…"
value = { historySeq || '' } onChange = { e = > setHistorySeq ( Number ( e . target . value ) || 0 ) } / >
) }
< / div >
{ historyMode ? (
< div >
{ ! historyData ? < div className = "empty-note" > 加 载 中 … < / div > : (
< div >
< SectionLabel > 截 至 seq { historyData . atSeq } ( 当 前 水 位 { historyData . watermark } ) < / SectionLabel >
{ historyData . countOnlySnapshots ? < div className = "rdm-audit-truncated" > 含 { historyData . countOnlySnapshots } 个 旧 计 数 快 照 ( 不 可 作 恢 复 依 据 ) < / div > : null }
{ historyData . formalMissing ? < div className = "empty-note" > { historyData . formalMissing } < / div > : null }
{ historyData . object ? (
< div className = "rdm-audit-detail-body" >
< div > 标 题 : { historyData . object . title } < / div >
< div > 状 态 : { historyData . object . state } < / div >
{ historyData . object . contentPreview && < div > 内 容 预 览 : { historyData . object . contentPreview } < / div > }
< / div >
) : historyData . empty ? < EmptyState icon = "folder" > 该 时 点 无 事 件 < / EmptyState > :
< div className = "empty-note" > 输 入 对 象 kind :id 查 看 历 史 快 照 < / div > }
{ historyData . formal && (
< div className = "rdm-audit-formal" >
正 式 版 R { historyData . formal . rev } ( 内 容 v { historyData . formal . seq } ) — — { historyData . formal . actor } 于 { new Date ( historyData . formal . at ) . toLocaleString ( 'zh-CN' ) }
< / div >
) }
< / div >
) }
< / div >
) : (
< div >
{ err && < EmptyState icon = "alert" > { err } < / EmptyState > }
{ ! err && items . length === 0 && < EmptyState icon = "clock" > 暂 无 事 件 < / EmptyState > }
{ items . map ( ( item , i ) = > {
if ( 'events' in item && Array . isArray ( ( item as OpGroup ) . events ) ) {
return < OpGroupView key = { item . operationId || i } group = { item as OpGroup } / >
}
return < EventRow key = { ( item as EventDTO ) . seq } ev = { item as EventDTO } / >
} ) }
{ hasMore && < button className = "btn v-ghost rdm-audit-more" onClick = { ( ) = > load ( cursor , false ) } > 加 载 更 多 < / button > }
< / div >
) }
< / div >
)
}
function OpGroupView ( { group } : { group : OpGroup } ) {
const [ open , setOpen ] = useState ( false )
return (
< div className = "rdm-audit-opgroup" >
< button className = "rdm-audit-opgroup-head" onClick = { ( ) = > setOpen ( ! open ) } >
< span className = "rdm-audit-opgroup-actor" > { group . actor } < / span >
< span className = "rdm-audit-opgroup-time" > { new Date ( group . at ) . toLocaleString ( 'zh-CN' ) } < / span >
< Chip tone = "plain" > { group . events . length } 个 事 件 < / Chip >
< / button >
{ open && group . events . map ( ev = > < EventRow key = { ev . seq } ev = { ev } / > ) }
< / div >
)
}
function EventRow ( { ev } : { ev : EventDTO } ) {
return (
< div className = "rdm-audit-event" >
< span className = "rdm-audit-event-time" > { new Date ( ev . at ) . toLocaleTimeString ( 'zh-CN' ) } < / span >
< span className = "rdm-audit-event-op" > { ev . op } < / span >
< span className = "rdm-audit-event-actor" > { ev . actor } < / span >
{ ev . detail && < span className = "rdm-audit-event-detail" > { ev . detail . slice ( 0 , 80 ) } < / span > }
{ ev . disclosure !== 'ok' && < Chip tone = "warn" > { ev . disclosure === 'model-candidate' ? '模型候选' : '来源受限' } < / Chip > }
< / div >
)
}
// ---- 对象详情视图 ----
function ObjectDetailView ( { detail } : { detail : ObjectDetail } ) {
return (
< div className = "rdm-audit-detail-body" >
< div > < strong > { detail . title || detail . id } < / strong > < / div >
< div > { detail . kind } : { detail . id } { detail . ver ? ` @ ${ detail . ver } ` : '' } < / div >
{ detail . state && < div > 状 态 : < Chip tone = { detail . state === 'baselined' ? 'ok' : 'plain' } > { detail . state } < / Chip > < / div > }
{ detail . labels && < div > 标 签 : { detail . labels . stage } { detail . labels . topics ? . length ? ` / ${ detail . labels . topics . join ( ', ' ) } ` : '' } < / div > }
{ detail . versions && detail . versions . length > 0 && (
< div >
< SectionLabel > 版 本 链 ( { detail . versions . length } ) < / SectionLabel >
{ detail . versions . map ( ( v : any , i : number ) = > (
< div key = { i } className = "rdm-audit-version" >
v { v . seq } · { v . actor } · { new Date ( v . at ) . toLocaleString ( 'zh-CN' ) }
< / div >
) ) }
< / div >
) }
{ detail . formal && (
< div className = "rdm-audit-formal" >
正 式 版 R { detail . formal . rev } ( 内 容 v { detail . formal . seq } ) — — { detail . formal . actor } 于 { new Date ( detail . formal . at ) . toLocaleString ( 'zh-CN' ) }
< / div >
) }
{ detail . relations && detail . relations . length > 0 && (
< div >
< SectionLabel > 一 跳 关 系 ( { detail . relations . length } ) < / SectionLabel >
{ detail . relations . map ( ( r , i ) = > < RelationRow key = { i } r = { r } / > ) }
< / div >
) }
{ detail . recentEvents && detail . recentEvents . length > 0 && (
< div >
< SectionLabel > 最 近 事 件 < / SectionLabel >
{ detail . recentEvents . map ( ev = > < EventRow key = { ev . seq } ev = { ev } / > ) }
< / div >
) }
< / div >
)
}
// ---- 覆盖范围标注(不冒充完整) ----
function CoverageList ( { cov } : { cov : CoverageItem [ ] } ) {
return (
< div className = "rdm-audit-coverage" >
< SectionLabel > 覆 盖 范 围 < / SectionLabel >
{ cov . map ( c = > (
< div key = { c . face } className = "rdm-audit-coverage-item" >
< Chip tone = "plain" > { c . face } < / Chip >
< span > { c . reason } < / span >
< / div >
) ) }
< / div >
)
}