101 lines
3.6 KiB
TypeScript
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>
|
|
)
|
|
}
|