@@ -1,24 +1,13 @@
'use client' ;
import { useEffect , useMemo } from 'react' ;
import { useEffect , useMemo , useState } from 'react' ;
import { useParams , useRouter } from 'next/navigation' ;
import { ChevronLeft , Package , Calendar , Clock , Users , Tag } from 'lucide-react' ;
import { ChevronLeft , Package , Calendar , Clock , Users , Tag , ChevronDown } from 'lucide-react' ;
import { useProductStore } from '@/stores/useProductStore' ;
import { getProjectDetail } from '@/lib/derive' ;
import { getProjectDetail , VersionWithContext } from '@/lib/derive' ;
import { Stage , Role , STAGES , ROLES , STAGE_INDEX , ROLE_LABEL } from '@/lib/stage' ;
import { VersionStatus , VERSION_STATUS_LABEL , VERSION_STATUS_BG } from '@/lib/version-status' ;
interface VersionWithContext {
id : string ; name : string ; status : VersionStatus ;
releaseDate : string | null ; createdAt : string ;
productId : string ; productName : string ; projectName : string ;
currentStage? : Stage ;
startDate? : string | null ;
expectedReleaseDate? : string | null ;
members ? : { role : Role ; name : string } [ ] ;
progress ? : { role : Role ; percent : number ; daysSpent : number } [ ] ;
}
/* ─── StatCard ─── */
function StatCard ( { value , label } : { value : number | string ; label : string } ) {
return (
@@ -29,16 +18,13 @@ function StatCard({ value, label }: { value: number | string; label: string }) {
) ;
}
/* ─── ProgressBar ─── */
/* ─── ProgressBar (for expanded released cards) ─── */
function ProgressBar ( { role , percent , daysSpent } : { role : Role ; percent : number ; daysSpent : number } ) {
return (
< div className = "flex items-center gap-2" >
< span className = "w-12 text-xs text-[var(--ink-soft)] shrink-0" > { ROLE_LABEL [ role ] } < / span >
< div className = "flex-1 h-2 rounded-full bg-zinc-100 overflow-hidden" >
< div
className = "h-full rounded-full bg-blue-500 transition-all"
style = { { width : ` ${ percent } % ` } }
/ >
< div className = "h-full rounded-full bg-blue-500 transition-all" style = { { width : ` ${ percent } % ` } } / >
< / div >
< span className = "text-xs text-[var(--ink-muted)] w-8 text-right" > { percent } % < / span >
< span className = "text-xs text-[var(--ink-muted)] w-10 text-right" >
@@ -48,60 +34,91 @@ function ProgressBar({ role, percent, daysSpent }: { role: Role; percent: number
) ;
}
/* ─── StagePipeline ─── */
function StagePipeline ( { currentStage } : { currentStage? : Stage } ) {
const currentIdx = currentStage !== undefined ? STAGE_INDEX [ currentStage ] : - 1 ;
/* ─── CapsuleStages (胶囊分段条) ─── */
function CapsuleStages ( { currentStage , progress } : {
currentStage? : Stage ;
progress ? : { role : Role ; percent : number ; daysSpent : number } [ ] ;
} ) {
const currentIdx = currentStage !== undefined
? ( currentStage === 'released' ? STAGES.length : STAGE_INDEX [ currentStage ] )
: - 1 ;
const progressMap = ( progress ? ? [ ] ) . reduce < Record < Role , { percent : number ; daysSpent : number } > > ( ( acc , p ) = > {
acc [ p . role ] = { percent : p.percent , daysSpent : p.daysSpent } ;
return acc ;
} , { } as Record < Role , { percent : number ; daysSpent : number } > ) ;
const stageLabel : Record < Stage , string > = {
requirement : '需求' ,
product_design : '产品设计' ,
ui_design : 'UI设计' ,
dev : '开发' ,
integration : '联调' ,
testing : '测试' ,
released : '已发布' ,
const stageRoleMap : Record < Stage , Role [ ] > = {
requirement : [ 'product' ] ,
product_design : [ 'product' ] ,
ui_design : [ 'ui' ] ,
dev : [ 'frontend' , 'backend' ] ,
integration : [ 'frontend' , 'backend' ] ,
testing : [ 'testing' ] ,
released : [ ] ,
} ;
function getStageInfo ( stage : Stage ) {
const roles = stageRoleMap [ stage ] ;
if ( roles . length === 0 ) return { percent : 0 , days : 0 , hasData : false } ;
const items = roles . map ( ( r ) = > progressMap [ r ] ) . filter ( Boolean ) ;
if ( items . length === 0 ) return { percent : 0 , days : 0 , hasData : false } ;
const percent = Math . round ( items . reduce ( ( s , i ) = > s + i . percent , 0 ) / items . length ) ;
const days = Math . max ( . . . items . map ( ( i ) = > i . daysSpent ) ) ;
return { percent , days , hasData : true } ;
}
return (
< div className = "flex items-center w-full py-2 " >
< div className = "flex rounded-lg border border-[var(--line)] overflow-hidden bg-[var(--bg-card)] " >
{ STAGES . map ( ( stage , idx ) = > {
const isCompleted = idx < currentIdx ;
const isCurrent = idx === currentIdx ;
const isFuture = idx > currentIdx ;
const info = getStageInfo ( stage . key ) ;
return (
< div key = { stage . key } className = "flex items-center flex-1 last:flex-none" >
< div className = "flex flex-col items-center gap-1" >
{ isCurrent ? (
< div className = "relative flex items-center justify-center" >
< div className = "absolute h-4 w-4 rounded-full bg-blue-500/30 animate-pulse" / >
< div className = "relative h-3 w-3 rounded-full bg-blue-500 ring-2 ring-blue-200" / >
< / div >
) : isCompleted ? (
< div className = "h-2.5 w-2.5 rounded-full bg-blue-500" / >
) : (
< div className = "h-2.5 w-2.5 rounded-full bg-zinc-200" / >
) }
< span
className = { ` text-[10px] whitespace-nowrap ${
isCurrent
? 'text-blue-600 font-medium'
: isCompleted
? 'text-[var(--ink-soft)]'
: 'text-[var(--ink-muted)]'
} ` }
< div
key = { stage . key }
className = { ` flex-1 flex flex-col ${ idx < STAGES . length - 1 ? 'border-r border-[var(--line-soft)]' : '' } ` }
>
< div className = "flex items-center justify-between px-2 py-1.5 min-h-[28px]" >
< span className = { ` text-[10px] font-medium leading-tight ${ isCurrent ? 'text-[var(--ink)]' : isCompleted ? 'text-[var(--ink-soft)]' : 'text-[var(--ink-muted)]' } ` } >
{ stage . label }
< / span >
< span className = { ` text-[9px] leading-tight ${ isCompleted ? 'text-emerald-600' : isCurrent ? 'text-blue-600 font-medium' : 'text-[var(--ink-muted)]' } ` } >
{ isCompleted ? ( info . hasData ? ` ${ info . days } 天 ` : '-' ) : isCurrent ? ( info . hasData ? ` ${ info . percent } % ` : '-' ) : '' }
< / span >
< / div >
< div className = "h-[3px] w-full bg-zinc-50" >
{ isCompleted && < div className = "h-full bg-zinc-700 w-full" / > }
{ isCurrent && info . hasData && (
< div className = "h-full bg-blue-100 w-full" >
< div className = "h-full bg-blue-500 transition-all" style = { { width : ` ${ info . percent } % ` } } / >
< / div >
{ idx < STAGES . length - 1 && (
< div
className = { ` h-0.5 flex-1 mx-1 mb-4 ${
isCompleted ? 'bg-blue-500' : 'bg-zinc-200'
} ` }
/ >
) }
< / div >
< / div >
) ;
} ) }
< / div >
) ;
}
/* ─── MemberChips (compact inline for version cards) ─── */
function MemberChips ( { members } : { members : { role : Role ; name : string } [ ] } ) {
if ( ! members || members . length === 0 ) return null ;
const grouped = ROLES . reduce < Record < Role , string [ ] > > ( ( acc , r ) = > {
acc [ r . key ] = members . filter ( ( m ) = > m . role === r . key ) . map ( ( m ) = > m . name ) ;
return acc ;
} , { } as Record < Role , string [ ] > ) ;
return (
< div className = "flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px]" >
{ ROLES . map ( ( r ) = > {
const names = grouped [ r . key ] ;
if ( ! names || names . length === 0 ) return null ;
return (
< span key = { r . key } className = "inline-flex items-center gap-1 text-[var(--ink-soft)]" >
< span className = "font-medium text-[var(--ink-muted)]" > { r . label } < / span >
{ names . join ( '/' ) }
< / span >
) ;
} ) }
< / div >
@@ -110,142 +127,174 @@ function StagePipeline({ currentStage }: { currentStage?: Stage }) {
/* ─── VersionCard ─── */
function VersionCard ( { version } : { version : VersionWithContext } ) {
const statusBg = VERSION_STATUS_BG [ version . status ] ;
const statusLabel = VERSION_STATUS_LABEL [ version . status ] ;
// Mode: planned
if ( version . status === 'planned' ) {
return (
< div className = "rounded-xl border border-dashed border-[var(--line)] p-4" >
< div className = "flex items-center gap-2" >
< span className = "font-semibold text-[var(--ink)]" > { version . name } < / span >
< span className = { ` text-xs px-2 py-0.5 rounded-full ${ statusBg } ` } >
{ statusLabel }
< / span >
< span className = "text-xs text-[var(--ink-muted)] ml-2" > 暂 无 详 情 < / span >
< / div >
< / div >
) ;
}
const [ expanded , setExpanded ] = useState ( false ) ;
const totalDays = ( version . progress ? ? [ ] ) . reduce ( ( sum , p ) = > sum + p . daysSpent , 0 ) ;
// Mode: released
const stageLabel = version . currentStage ? STAGES . find ( ( s ) = > s . key === version . currentStage ) ? . label ? ? '' : '' ;
const displayStatus = version . status === 'released' ? '已发布' : version . status === 'planned' ? '规划中' : stageLabel ? ` ${ stageLabel } 中 ` : '开发中' ;
const displayBg = version . status === 'released' ? 'bg-zinc-100 text-zinc-600' : version . status === 'planned' ? 'bg-orange-500/10 text-orange-600' : 'bg-blue-500/10 text-blue-600' ;
if ( version . status === 'planned' ) {
return (
< div className = "rounded-xl border border-dashed border-[var(--line)] px-4 py-3" >
< div className = "flex items-center gap-2" >
< span className = "text-sm font-medium text-[var(--ink)]" > { version . name } < / span >
< span className = { ` text-[11px] px-2 py-0.5 rounded-full ${ displayBg } ` } > { displayStatus } < / span >
< span className = "text-[11px] text-[var(--ink-muted)]" > 暂 无 详 情 < / span >
< / div >
< / div >
) ;
}
if ( version . status === 'released' ) {
return (
< div className = "rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 " >
< div className = "flex items-center gap-2 mb-2" >
< span className = "font-semibold text-[var(--ink)]" > { version . name } < / span >
< span className = { ` text-xs px-2 py-0.5 rounded-full ${ statusBg } ` } >
{ statusLabel }
< / span >
< / div >
< div className = "text-xs text-[var(--ink-muted)] mb-2 flex items-center gap-1" >
< div className = "rounded-xl border border-[var(--line)] bg-[var(--bg-card)] overflow-hidden " >
< button
type = "button"
onClick = { ( ) = > setExpanded ( ! expanded ) }
className = "w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-[var(--bg-hover)] transition-colors"
>
< span className = "text-sm font-medium text-[var(--ink)]" > { version . name } < / span >
< span className = { ` text-[11px] px-2 py-0.5 rounded-full ${ displayBg } ` } > { displayStatus } < / span >
< span className = "flex-1 text-[11px] text-[var(--ink-muted)] flex items-center gap-1" >
< Calendar className = "h-3 w-3" / >
< span >
{ version . startDate ? ? '-' } → { version . releaseDate ? ? '-' } 发 布 总 { totalDays } 天
{ version . startDate ? ? '-' } → { version . releaseDate ? ? '-' }
< span className = "ml-1" > 共 { totalDays } 天 < / span >
< / span >
< /div >
{ version . members && version . members . length > 0 && (
< div className = "text-xs text-[var(--ink-soft)]" >
{ version . members
. map ( ( m ) = > ` ${ ROLE_LABEL [ m . role ] } ${ m . name } ` )
. join ( ' · ' ) }
< ChevronDown className = { ` h-3.5 w-3.5 text-[var(--ink-muted)] transition-transform ${ expanded ? 'rotate-180' : '' } ` } / >
< / button >
{ expanded && (
< div className = "px-4 pb-4 pt-2 border-t border-[var(--line-soft)] space-y-3" >
< CapsuleStages currentStage = { version . currentStage } progress = { version . progress } / >
< MemberChips members = { version . members ? ? [ ] } / >
< / div >
) }
< / div >
) ;
}
// Mode: developing
return (
< div className = "rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-5 shadow-sm" >
< div className = "rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 shadow-sm" >
< div className = "flex items-center gap-2 mb-3" >
< span className = "font-semibold text-[var(--ink)]" > { version . name } < / span >
< span className = { ` text-xs px-2 py-0.5 rounded-full ${ status Bg} ` } >
{ statusLabel }
< / span >
< span className = "text-sm font-medium text-[var(--ink)]" > { version . name } < / span >
< span className = { ` text-[11px] px-2 py-0.5 rounded-full ${ display Bg} ` } > { displayStatus } < / span >
< / div >
< div className = "mb-4 " >
< StagePipeline currentStage = { version . currentStage } / >
< / div >
< div className = "flex items-center justify-between text-xs text-[var(--ink-muted)] mb-3 pb-3 border-b border-[var(--line-soft)]" >
< div className = "flex items-center gap-1" >
< div className = "mb-3" > < CapsuleStages currentStage = { version . currentStage } progress = { version . progress } / > < / div >
< div className = "flex items-center justify-between text-[11px] text-[var(--ink-muted)] mb-3 pb-3 border-b border-[var(--line-soft)] " >
< span className = "flex items-center gap-1" >
< Calendar className = "h-3 w-3" / >
< span >
{ version . startDate ? ? '-' } 开 始 → 预 计 { version . expectedReleaseDate ? ? '-' }
{ version . startDate ? ? '-' } → 预 计 { version . expectedReleaseDate ? ? '-' }
< / span >
< span className = "flex items-center gap-1" >
< Clock className = "h-3 w-3" / > 已 耗 时 { totalDays } 天
< / span >
< / div >
< div className = "flex items-center gap-1" >
< Clock className = "h-3 w-3" / >
< span > 已 耗 时 { totalDays } 天 < / span >
< / div >
< / div >
{ version . members && version . members . length > 0 && (
< div className = "text-xs text-[var(--ink-soft)] mb-4" >
{ version . members
. map ( ( m ) = > ` ${ ROLE_LABEL [ m . role ] } ${ m . name } ` )
. join ( ' · ' ) }
< / div >
) }
{ version . progress && version . progress . length > 0 && (
< div className = "space-y-2" >
{ version . progress . map ( ( p ) = > (
< ProgressBar
key = { p . role }
role = { p . role }
percent = { p . percent }
daysSpent = { p . daysSpent }
/ >
) ) }
< / div >
) }
< MemberChips members = { version . members ? ? [ ] } / >
< / div >
) ;
}
/* ─── Main Page Component ─── */
/* ─── TeamSection (compact, overflow with tooltip) ─── */
function TeamSection ( { teamByRole } : { teamByRole : Record < string , Record < string , number > > } ) {
const MAX_VISIBLE = 4 ;
return (
< section >
< div className = "flex items-center gap-2 mb-3" >
< Users className = "h-4 w-4 text-[var(--ink-soft)]" / >
< h2 className = "text-sm font-semibold text-[var(--ink)]" > 项 目 人 员 < / h2 >
< / div >
< div className = "rounded-xl border border-[var(--line)] bg-[var(--bg-card)] px-4 py-3" >
< div className = "flex flex-wrap items-center gap-x-5 gap-y-2" >
{ ROLES . map ( ( role ) = > {
const peopleMap = teamByRole [ role . key ] || { } ;
const people = Object . entries ( peopleMap ) . sort ( ( a , b ) = > ( b [ 1 ] as number ) - ( a [ 1 ] as number ) ) ;
if ( people . length === 0 ) return null ;
const visible = people . slice ( 0 , MAX_VISIBLE ) ;
const hidden = people . slice ( MAX_VISIBLE ) ;
return (
< div key = { role . key } className = "flex items-center gap-1.5" >
< span className = "text-[11px] font-medium text-[var(--ink-muted)]" > { role . label } < / span >
< div className = "flex items-center gap-1" >
{ visible . map ( ( [ name , count ] ) = > (
< span key = { name } className = "inline-flex items-center h-5 px-1.5 rounded bg-[var(--bg-subtle)] text-[11px] text-[var(--ink-soft)]" >
{ name } < span className = "text-[var(--ink-muted)] ml-0.5" > { '× ' } { count as number } < / span >
< / span >
) ) }
{ hidden . length > 0 && (
< span className = "relative group" >
< span className = "inline-flex items-center h-5 px-1.5 rounded bg-[var(--bg-subtle)] text-[11px] text-[var(--ink-muted)] cursor-default" >
+ { hidden . length }
< / span >
< span className = "absolute bottom-full left-1/2 -translate-x-1/2 mb-1.5 hidden group-hover:flex flex-col items-center z-10" >
< span className = "whitespace-nowrap rounded-lg bg-zinc-800 px-2.5 py-1.5 text-[11px] text-white shadow-lg" >
{ hidden . map ( ( [ n , c ] ) = > ` ${ n } (× ${ c } ) ` ) . join ( ', ' ) }
< / span >
< span className = "h-1.5 w-1.5 rotate-45 bg-zinc-800 -mt-1" / >
< / span >
< / span >
) }
< / div >
< / div >
) ;
} ) }
< / div >
< / div >
< / section >
) ;
}
/* ─── Status Filter ─── */
const FILTER_OPTIONS : { key : string ; label : string } [ ] = [
{ key : 'all' , label : '全部' } ,
{ key : 'requirement' , label : '调研' } ,
{ key : 'product_design' , label : '产品设计' } ,
{ key : 'ui_design' , label : 'UI设计' } ,
{ key : 'dev' , label : '开发' } ,
{ key : 'integration' , label : '联调' } ,
{ key : 'testing' , label : '测试' } ,
{ key : 'released' , label : '已发布' } ,
{ key : 'planned' , label : '规划中' } ,
] ;
/* ─── Main Page ─── */
export default function ProjectDetailPage() {
const params = useParams ( ) ;
const router = useRouter ( ) ;
const projectId = params . id as string ;
const { overview , fetchOverview } = useProductStore ( ) ;
const [ statusFilter , setStatusFilter ] = useState < string > ( 'all' ) ;
useEffect ( ( ) = > {
fetchOverview ( ) ;
} , [ fetchOverview ] ) ;
useEffect ( ( ) = > { fetchOverview ( ) ; } , [ fetchOverview ] ) ;
const project = useMemo (
( ) = > getProjectDetail ( overview , projectId ) ,
[ overview , projectId ]
) ;
const project = useMemo ( ( ) = > getProjectDetail ( overview , projectId ) , [ overview , projectId ] ) ;
const sortedVersions = useMemo ( ( ) = > {
if ( ! project ) return [ ] ;
return [ . . . project . versions ] . sort (
( a , b ) = > new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( )
) ;
} , [ project ] ) ;
let list = [ . . . project . versions ] ;
if ( statusFilter !== 'all' ) {
if ( statusFilter === 'planned' ) {
list = list . filter ( ( v ) = > v . status === 'planned' ) ;
} else if ( statusFilter === 'released' ) {
list = list . filter ( ( v ) = > v . status === 'released' ) ;
} else {
list = list . filter ( ( v ) = > v . status === 'developing' && v . currentStage === statusFilter ) ;
}
}
return list . sort ( ( a , b ) = > new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( ) ) ;
} , [ project , statusFilter ] ) ;
const stats = useMemo ( ( ) = > {
if ( ! project ) return { total : 0 , developing : 0 , released : 0 , totalDays : 0 } ;
const total = project . versions . length ;
const developing = project . versions . filter ( ( v ) = > v . status === 'developing' ) . length ;
const released = project . versions . filter ( ( v ) = > v . status === 'released' ) . length ;
const totalDays = project . versions . reduce ( ( sum , v ) = > {
return sum + ( v . progress ? ? [ ] ) . reduce ( ( s , p ) = > s + p . daysSpent , 0 ) ;
} , 0 ) ;
const totalDays = project . versions . reduce ( ( sum , v ) = > sum + ( v . progress ? ? [ ] ) . reduce ( ( s , p ) = > s + p . daysSpent , 0 ) , 0 ) ;
return { total , developing , released , totalDays } ;
} , [ project ] ) ;
// Team members aggregation
const teamByRole = useMemo ( ( ) = > {
if ( ! project ) return { } ;
const map : Record < Role , Record < string , number > > = { } as any ;
if ( ! project ) return { } as Record < string , Record < string , number > > ;
const map : Record < string , Record < string , number > > = { } ;
ROLES . forEach ( ( r ) = > ( map [ r . key ] = { } ) ) ;
project . versions . forEach ( ( v ) = > {
( v . members ? ? [ ] ) . forEach ( ( m ) = > {
@@ -260,43 +309,28 @@ export default function ProjectDetailPage() {
return (
< div className = "flex h-full flex-col items-center justify-center gap-3" >
< p className = "text-sm text-[var(--ink-muted)]" > 项 目 不 存 在 < / p >
< button
onClick = { ( ) = > router . push ( '/projects' ) }
className = "text-xs text-[var(--accent)] hover:underline"
>
返 回 项 目 列 表
< / button >
< button onClick = { ( ) = > router . push ( '/projects' ) } className = "text-xs text-[var(--accent)] hover:underline" > 返 回 项 目 列 表 < / button >
< / div >
) ;
}
return (
< div className = "flex h-full flex-col" >
{ /* Header */ }
< header className = "flex h-14 shrink-0 items-center justify-between border-b border-[var(--line)] bg-[var(--bg-card)] px-5" >
< div className = "flex items-center" >
< button
onClick = { ( ) = > router . push ( '/projects' ) }
className = "flex items-center gap-1 rounded-md px-1.5 py-1 text-[12.5px] text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
>
< ChevronLeft className = "h-3.5 w-3.5" strokeWidth = { 2 } / >
项 目
< button onClick = { ( ) = > router . push ( '/projects' ) } className = "flex items-center gap-1 rounded-md px-1.5 py-1 text-[12.5px] text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]" >
< ChevronLeft className = "h-3.5 w-3.5" strokeWidth = { 2 } / > 项 目
< / button >
< span className = "ml-2 text-[var(--ink-muted)]" > / < / span >
< span className = "ml-2 text-[15px] font-semibold text-[var(--ink)]" >
{ project . name }
< / span >
< span className = "ml-2 text-[15px] font-semibold text-[var(--ink)]" > { project . name } < / span >
< / div >
< div className = "flex items-center gap-1.5 rounded-full bg-[var(--bg-subtle)] px-2.5 py-1 text-xs text-[var(--ink-soft)]" >
< Package className = "h-3 w-3" / >
< span > { project . productName } < / span >
< Package className = "h-3 w-3" / > < span > { project . productName } < / span >
< / div >
< / header >
{ /* Content */ }
< div className = "flex-1 overflow-y-auto p-5 bg-[var(--bg)]" >
< div className = "max-w-4xl mx-auto space-y-6 " >
{ /* Overview Stats Row */ }
< div className = "space-y-5 " >
< div className = "grid grid-cols-4 gap-4" >
< StatCard value = { stats . total } label = "总版本数" / >
< StatCard value = { stats . developing } label = "进行中" / >
@@ -304,52 +338,29 @@ export default function ProjectDetailPage() {
< StatCard value = { stats . totalDays } label = "总耗时(天)" / >
< / div >
{ /* Team Members Section * /}
< section >
< div className = "flex items-center gap-2 mb-3" >
< Users className = "h-4 w-4 text-[var(--ink-soft)]" / >
< h2 className = "text-sm font-semibold text-[var(--ink)]" > 项 目 人 员 < / h2 >
< / div >
< div className = "rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-5 space-y-3" >
{ ROLES . map ( ( role ) = > {
const peopleMap = ( teamByRole as any ) [ role . key ] || { } ;
const people = Object . entries ( peopleMap ) . sort ( ( a , b ) = > ( b [ 1 ] as number ) - ( a [ 1 ] as number ) ) ;
return (
< div key = { role . key } className = "flex items-start gap-3" >
< span className = "w-12 text-xs text-[var(--ink-soft)] shrink-0 pt-1" >
{ role . label }
< / span >
< div className = "flex flex-wrap gap-1.5 flex-1" >
{ people . length === 0 ? (
< span className = "text-xs text-[var(--ink-muted)]" > - < / span >
) : (
people . map ( ( [ name , count ] ) = > (
< span
key = { name }
className = "rounded-full bg-[var(--bg-subtle)] px-2 py-0.5 text-xs text-[var(--ink-soft)]"
>
{ name } ( { count as number } 次 )
< / span >
) )
) }
< / div >
< / div >
) ;
} ) }
< / div >
< / section >
< TeamSection teamByRole = { teamByRole } />
{ /* Version Timeline Section */ }
< section >
< div className = "flex items-center gap-2 mb-3" >
< div className = "flex items-center justify-between mb-3" >
< div className = "flex items-center gap-2" >
< Tag className = "h-4 w-4 text-[var(--ink-soft)]" / >
< h2 className = "text-sm font-semibold text-[var(--ink)]" > 版 本 记 录 < / h2 >
< / div >
< div className = "space-y-3 " >
{ sortedVersions . length === 0 ? (
< div className = "rounded-xl border border-dashed border-[var(--line)] p-6 text-center text-xs text-[var(--ink-muted)]" >
暂 无 版 本
< div className = "flex items-center gap-0.5 rounded-lg bg-[var(--bg-subtle)] p-0.5 " >
{ FILTER_OPTIONS . map ( ( opt ) = > (
< button
key = { opt . key }
onClick = { ( ) = > setStatusFilter ( opt . key ) }
className = { ` px-2.5 py-1 rounded-md text-[11px] font-medium transition-colors ${ statusFilter === opt . key ? 'bg-[var(--bg-card)] text-[var(--ink)] shadow-sm' : 'text-[var(--ink-muted)] hover:text-[var(--ink-soft)]' } ` }
>
{ opt . label }
< / button >
) ) }
< / div >
< / div >
< div className = "space-y-2.5" >
{ sortedVersions . length === 0 ? (
< div className = "rounded-xl border border-dashed border-[var(--line)] p-6 text-center text-xs text-[var(--ink-muted)]" > 暂 无 版 本 < / div >
) : (
sortedVersions . map ( ( v ) = > < VersionCard key = { v . id } version = { v } / > )
) }