Files
meetnote/web/components/meeting-card.tsx
2026-04-13 19:00:17 +08:00

101 lines
3.6 KiB
TypeScript

'use client'
import Link from 'next/link'
import { Clock, Users, Mic } from 'lucide-react'
import type { Meeting } from '@/lib/mock-data'
import { formatDuration } from '@/lib/mock-data'
import { cn } from '@/lib/utils'
const statusConfig = {
pending: { label: '排队中', color: 'text-muted-foreground', dot: 'bg-muted-foreground/60' },
uploading: { label: '上传中', color: 'text-info', dot: 'bg-info' },
transcribing: { label: '转写中', color: 'text-warning', dot: 'bg-warning animate-pulse' },
summarizing: { label: '总结中', color: 'text-warning', dot: 'bg-warning animate-pulse' },
done: { label: '已完成', color: 'text-success', dot: 'bg-success' },
failed: { label: '失败 · 重试', color: 'text-danger', dot: 'bg-danger' },
}
export function MeetingCard({ meeting }: { meeting: Meeting }) {
const cfg = statusConfig[meeting.status]
const time = new Date(meeting.date).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
const isProcessing =
meeting.status === 'transcribing' || meeting.status === 'summarizing'
return (
<Link
href={`/meetings/${meeting.id}`}
className={cn(
'block rounded-2xl border bg-surface-elevated p-5 transition-all',
'border-border hover:border-primary/30 hover:shadow-sm',
meeting.status === 'failed' && 'border-danger/40',
)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Mic className="h-4 w-4 text-muted-foreground shrink-0" />
<h3 className="text-[17px] font-semibold truncate">{meeting.title}</h3>
</div>
<div className="mt-1.5 flex items-center gap-3 text-[13px] text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
{time} · {formatDuration(meeting.duration)}
</span>
{meeting.participants && meeting.participants.length > 0 && (
<span className="flex items-center gap-1 truncate">
<Users className="h-3.5 w-3.5" />
{meeting.participants.join(' · ')}
</span>
)}
</div>
</div>
<div className={cn('flex items-center gap-1.5 text-[13px] font-medium', cfg.color)}>
<span className={cn('h-2 w-2 rounded-full', cfg.dot)} />
<span>
{cfg.label}
{isProcessing && meeting.chunksTotal
? ` ${meeting.chunksDone}/${meeting.chunksTotal}`
: ''}
</span>
</div>
</div>
{/* Progress bar for processing */}
{isProcessing && meeting.progress != null && (
<div className="mt-4 h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${meeting.progress}%` }}
/>
</div>
)}
{/* Summary preview for done */}
{meeting.status === 'done' && meeting.summary && (
<>
<p className="mt-4 text-[14px] leading-[1.7] text-muted-foreground line-clamp-2">
{meeting.summary.preview}
</p>
<div className="mt-3 flex flex-wrap gap-1.5">
{meeting.summary.keywords.map((kw) => (
<span
key={kw}
className="inline-flex items-center rounded-full bg-primary-subtle px-2.5 py-0.5 text-[11px] font-medium text-primary"
>
#{kw}
</span>
))}
</div>
</>
)}
</Link>
)
}