@@ -0,0 +1,205 @@
/ * *
* ToolchainPanel — 工 具 链 事 件 ( 系 统 级 )
* 展 示 _toolchain 项 目 的 tasks : CI / PR / 部 署 / Review 通 知
* /
import { useEffect , useState } from 'react' ;
const STATUS_COLORS : Record < string , string > = {
pending : '#f59e0b22' , claimed : '#6a9eff22' , working : '#6a9eff22' ,
review : '#818cf822' , done : '#2ecc8a22' , failed : '#ef444422' ,
cancelled : '#6b728022' , blocked : '#ef444422' ,
} ;
const STATUS_LABELS : Record < string , string > = {
pending : '待处理' , claimed : '已认领' , working : '处理中' ,
review : '审查中' , done : '已完成' , failed : '失败' ,
cancelled : '已取消' , blocked : '已拦截' ,
} ;
function fmtTime ( iso : string ) : string {
try {
const d = new Date ( iso . includes ( 'T' ) ? iso : iso.replace ( ' ' , 'T' ) + 'Z' ) ;
const now = Date . now ( ) ;
const diff = now - d . getTime ( ) ;
const mins = Math . floor ( diff / 60000 ) ;
if ( mins < 1 ) return '刚刚' ;
if ( mins < 60 ) return ` ${ mins } 分钟前 ` ;
const hrs = Math . floor ( mins / 60 ) ;
if ( hrs < 24 ) return ` ${ hrs } 小时前 ` ;
return ` ${ d . getMonth ( ) + 1 } / ${ d . getDate ( ) } ${ d . getHours ( ) } : ${ String ( d . getMinutes ( ) ) . padStart ( 2 , '0' ) } ` ;
} catch { return iso ; }
}
export default function ToolchainPanel() {
const [ tasks , setTasks ] = useState < any [ ] > ( [ ] ) ;
const [ selectedId , setSelectedId ] = useState < string | null > ( null ) ;
const [ detail , setDetail ] = useState < any > ( null ) ;
const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
const [ loading , setLoading ] = useState ( false ) ;
const loadTasks = async ( q? : string ) = > {
setLoading ( true ) ;
try {
const url = q
? ` /api/projects/_toolchain/tasks?q= ${ encodeURIComponent ( q ) } `
: ` /api/projects/_toolchain/tasks ` ;
const res = await fetch ( url ) ;
if ( res . ok ) {
const data = await res . json ( ) ;
setTasks ( data . tasks || [ ] ) ;
}
} catch { /* */ }
setLoading ( false ) ;
} ;
useEffect ( ( ) = > { loadTasks ( ) ; } , [ ] ) ;
// 搜索防抖 300ms
useEffect ( ( ) = > {
const timer = setTimeout ( ( ) = > {
if ( searchQuery !== undefined ) loadTasks ( searchQuery || undefined ) ;
} , 300 ) ;
return ( ) = > clearTimeout ( timer ) ;
} , [ searchQuery ] ) ;
useEffect ( ( ) = > {
if ( ! selectedId ) { setDetail ( null ) ; return ; }
( async ( ) = > {
try {
const res = await fetch (
` /api/projects/_toolchain/tasks/ ${ selectedId } ?expand=comments `
) ;
if ( res . ok ) setDetail ( await res . json ( ) ) ;
} catch { /* */ }
} ) ( ) ;
} , [ selectedId ] ) ;
// 渲染评论列表(兼容 expand 和裸 list 格式)
const renderComments = ( comments : any [ ] ) = > {
if ( ! comments || comments . length === 0 ) return null ;
return (
< div style = { { marginTop : 16 } } >
< div style = { { fontSize : 11 , color : 'var(--muted)' , marginBottom : 8 , fontWeight : 600 } } >
📋 处 理 记 录 ( { comments . length } )
< / div >
{ comments . map ( ( c : any , i : number ) = > (
< div key = { c . id || i } style = { {
padding : '8px 12px' , background : 'var(--panel2)' , borderRadius : 6 , marginBottom : 6 ,
} } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , marginBottom : 4 } } >
< span style = { { fontSize : 10 , color : 'var(--acc)' , fontWeight : 600 } } >
{ c . author || 'system' }
< / span >
< span style = { { fontSize : 9 , color : 'var(--muted)' } } > { fmtTime ( c . created_at ) } < / span >
< / div >
< div style = { { fontSize : 12 , color : '#a0aec0' , lineHeight : 1.5 } } > { c . body } < / div >
< / div >
) ) }
< / div >
) ;
} ;
return (
< div style = { { display : 'flex' , gap : 0 , height : '100%' , minHeight : 500 } } >
{ /* 左侧列表 */ }
< div style = { { width : 380 , borderRight : '1px solid var(--line)' , display : 'flex' , flexDirection : 'column' , flexShrink : 0 } } >
{ /* 搜索栏 + 刷新 */ }
< div style = { { padding : '10px 14px' , borderBottom : '1px solid var(--line)' , display : 'flex' , gap : 6 , alignItems : 'center' } } >
< input
type = "text"
placeholder = "搜索工具链事件..."
value = { searchQuery }
onChange = { e = > setSearchQuery ( e . target . value ) }
style = { {
flex : 1 , padding : '4px 8px' , borderRadius : 4 , fontSize : 11 ,
border : '1px solid #2a3550' , background : '#161b2e' , color : '#dde4f8' ,
outline : 'none' ,
} }
/ >
< button onClick = { ( ) = > loadTasks ( searchQuery || undefined ) } style = { {
padding : '3px 8px' , borderRadius : 4 , fontSize : 10 ,
border : '1px solid #2a3550' , background : '#161b2e' , color : '#8899aa' , cursor : 'pointer' ,
} } > 🔄 < / button >
< span style = { { fontSize : 10 , color : 'var(--muted)' } } > { tasks . length } 条 < / span >
< / div >
{ /* 事件列表 */ }
< div style = { { flex : 1 , overflowY : 'auto' } } >
{ tasks . length === 0 && (
< div style = { { textAlign : 'center' , padding : 40 , color : 'var(--muted)' , fontSize : 12 } } >
{ loading ? '加载中...' : '暂无工具链事件' }
< / div >
) }
{ tasks . map ( ( t : any ) = > (
< div key = { t . id } onClick = { ( ) = > setSelectedId ( t . id ) } style = { {
padding : '10px 14px' , borderBottom : '1px solid var(--line)' ,
cursor : 'pointer' , transition : 'background .15s' ,
background : selectedId === t . id ? 'var(--panel2)' : 'transparent' ,
} }
onMouseEnter = { e = > e . currentTarget . style . background = 'var(--panel2)' }
onMouseLeave = { e = > e . currentTarget . style . background = selectedId === t . id ? 'var(--panel2)' : 'transparent' }
>
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 4 } } >
< span style = { {
fontSize : 9 , padding : '1px 5px' , borderRadius : 3 ,
background : STATUS_COLORS [ t . status ] || '#2a3550' ,
color : '#dde4f8' ,
} } > { STATUS_LABELS [ t . status ] || t . status } < / span >
< span style = { { fontSize : 9 , color : 'var(--muted)' } } > { fmtTime ( t . created_at ) } < / span >
< / div >
< div style = { {
fontSize : 12 , fontWeight : 500 , color : '#dde4f8' ,
overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' ,
} } > { t . title } < / div >
< / div >
) ) }
< / div >
< / div >
{ /* 右侧详情 */ }
< div style = { { flex : 1 , padding : '16px 20px' , overflowY : 'auto' } } >
{ ! detail ? (
< div style = { { textAlign : 'center' , padding : 60 , color : 'var(--muted)' } } >
< div style = { { fontSize : 36 , marginBottom : 12 } } > ⛓ ️ < / div >
< div style = { { fontSize : 13 } } > 选 择 一 条 事 件 查 看 详 情 < / div >
< / div >
) : (
< >
{ /* 头部 */ }
< div style = { { marginBottom : 16 } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginBottom : 6 } } >
< span style = { { fontSize : 10 , padding : '2px 6px' , borderRadius : 4 , background : STATUS_COLORS [ detail . status ] || '#2a3550' , color : '#dde4f8' } } >
{ STATUS_LABELS [ detail . status ] || detail . status }
< / span >
< span style = { { fontSize : 10 , color : 'var(--muted)' } } > { detail . id } < / span >
< / div >
< div style = { { fontSize : 18 , fontWeight : 700 , lineHeight : 1.3 } } > { detail . title } < / div >
< div style = { { fontSize : 12 , color : 'var(--muted)' , marginTop : 6 } } >
{ fmtTime ( detail . created_at ) }
< / div >
< / div >
{ /* 正文 */ }
{ detail . description && (
< div style = { {
padding : '14px 16px' , background : 'var(--panel2)' , borderRadius : 10 ,
fontSize : 13 , color : '#a0aec0' , lineHeight : 1.7 , whiteSpace : 'pre-wrap' ,
} } >
{ detail . description }
< / div >
) }
{ /* action_report 评论 — expand 格式 {items, total_count} */ }
{ detail . comments && detail . comments . items && detail . comments . items . length > 0 &&
renderComments ( detail . comments . items )
}
{ /* 兼容裸 list 格式 */ }
{ detail . comments && Array . isArray ( detail . comments ) && detail . comments . length > 0 &&
renderComments ( detail . comments )
}
< / >
) }
< / div >
< / div >
) ;
}