257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
'use client'
|
||
|
||
import Link from 'next/link'
|
||
import { use, useState } from 'react'
|
||
import {
|
||
ArrowLeft,
|
||
Play,
|
||
Pause,
|
||
MoreHorizontal,
|
||
Star,
|
||
CheckSquare,
|
||
Target,
|
||
Hash,
|
||
Download,
|
||
} from 'lucide-react'
|
||
import { AppShell } from '@/components/app-shell'
|
||
import { getMeeting, formatDuration, formatTime, mockTranscript } from '@/lib/mock-data'
|
||
import { cn } from '@/lib/utils'
|
||
import { notFound } from 'next/navigation'
|
||
|
||
export default function MeetingDetailPage({
|
||
params,
|
||
}: {
|
||
params: Promise<{ id: string }>
|
||
}) {
|
||
const { id } = use(params)
|
||
const meeting = getMeeting(id)
|
||
const [playing, setPlaying] = useState(false)
|
||
const [tab, setTab] = useState<'transcript' | 'summary'>('transcript')
|
||
|
||
if (!meeting) notFound()
|
||
|
||
const done = meeting.status === 'done'
|
||
const summary = meeting.summary
|
||
|
||
return (
|
||
<AppShell>
|
||
<div className="flex h-screen flex-col">
|
||
{/* Header */}
|
||
<header className="border-b border-border bg-surface-elevated/95 backdrop-blur px-5 py-4 md:px-10">
|
||
<div className="mx-auto max-w-6xl">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div className="flex items-start gap-3 min-w-0">
|
||
<Link
|
||
href="/"
|
||
className="mt-1 flex h-8 w-8 items-center justify-center rounded-lg hover:bg-surface-muted shrink-0"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Link>
|
||
<div className="min-w-0">
|
||
<h1 className="text-[22px] font-semibold tracking-tight truncate">
|
||
{meeting.title}
|
||
</h1>
|
||
<p className="mt-1 text-[13px] text-muted-foreground">
|
||
{new Date(meeting.date).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false,
|
||
})}
|
||
{' · '}
|
||
{formatDuration(meeting.duration)}
|
||
{meeting.participants && ' · ' + meeting.participants.join(' ')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-surface-muted">
|
||
<MoreHorizontal className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Player */}
|
||
<div className="mt-5 flex items-center gap-4 rounded-2xl bg-surface-muted px-4 py-3">
|
||
<button
|
||
onClick={() => setPlaying(!playing)}
|
||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm hover:bg-primary-light"
|
||
>
|
||
{playing ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4 translate-x-0.5" />}
|
||
</button>
|
||
<div className="flex-1">
|
||
<div className="relative h-1.5 overflow-hidden rounded-full bg-muted">
|
||
<div className="absolute inset-y-0 left-0 w-[12%] rounded-full bg-primary" />
|
||
</div>
|
||
<div className="mt-1.5 flex items-center justify-between text-[11px] font-mono text-muted-foreground">
|
||
<span>{formatTime(600)}</span>
|
||
<span>{formatTime(meeting.duration)}</span>
|
||
</div>
|
||
</div>
|
||
<button className="hidden md:block rounded-lg px-2 py-1 text-[12px] font-medium text-muted-foreground hover:bg-background">
|
||
1x
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Tabs mobile */}
|
||
<div className="md:hidden flex border-b border-border bg-surface-elevated">
|
||
{(['transcript', 'summary'] as const).map((t) => (
|
||
<button
|
||
key={t}
|
||
onClick={() => setTab(t)}
|
||
className={cn(
|
||
'flex-1 py-3 text-[14px] font-medium transition-colors',
|
||
tab === t ? 'text-primary border-b-2 border-primary' : 'text-muted-foreground',
|
||
)}
|
||
>
|
||
{t === 'transcript' ? '转写' : '总结'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{done ? (
|
||
<div className="flex-1 overflow-auto">
|
||
<div className="mx-auto max-w-6xl px-5 py-6 md:px-10 md:py-8">
|
||
<div className="grid gap-6 md:grid-cols-5">
|
||
{/* Transcript */}
|
||
<div
|
||
className={cn(
|
||
'md:col-span-3',
|
||
tab !== 'transcript' && 'hidden md:block',
|
||
)}
|
||
>
|
||
<div className="mb-4 flex items-center gap-2">
|
||
<h2 className="text-[15px] font-semibold">转写全文</h2>
|
||
<span className="text-[12px] text-muted-foreground">
|
||
{mockTranscript.length} 段
|
||
</span>
|
||
<button className="ml-auto inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface-elevated px-3 py-1.5 text-[12px] font-medium text-muted-foreground hover:bg-surface-muted">
|
||
<Download className="h-3.5 w-3.5" />
|
||
下载 MD
|
||
</button>
|
||
</div>
|
||
<div className="space-y-5 max-w-[680px]">
|
||
{mockTranscript.map((seg, i) => (
|
||
<div key={i} className="group">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="text-[13px] font-semibold text-primary">
|
||
{seg.speaker}
|
||
</span>
|
||
<button className="font-mono text-[11px] text-muted-foreground hover:text-primary">
|
||
[{formatTime(seg.time)}]
|
||
</button>
|
||
</div>
|
||
<p className="text-[15px] leading-[1.75] text-foreground">{seg.text}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Summary - 4 cards */}
|
||
<aside
|
||
className={cn(
|
||
'md:col-span-2 space-y-4',
|
||
tab !== 'summary' && 'hidden md:block',
|
||
)}
|
||
>
|
||
{summary && (
|
||
<>
|
||
<SummaryCard icon={Star} title="要点" accent="text-primary">
|
||
<ul className="space-y-2">
|
||
{summary.keyPoints.map((p, i) => (
|
||
<li key={i} className="flex gap-2 text-[14px] leading-[1.6]">
|
||
<span className="text-primary shrink-0">•</span>
|
||
<span>{p}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</SummaryCard>
|
||
|
||
<SummaryCard icon={CheckSquare} title="待办" accent="text-success">
|
||
<ul className="space-y-3">
|
||
{summary.todos.map((t, i) => (
|
||
<li key={i} className="flex gap-2 text-[14px] leading-[1.5]">
|
||
<span className="mt-1 h-3.5 w-3.5 shrink-0 rounded border border-border-strong" />
|
||
<div>
|
||
<div>{t.text}</div>
|
||
{(t.owner || t.due) && (
|
||
<div className="mt-0.5 text-[12px] text-muted-foreground">
|
||
{t.owner && <span>{t.owner}</span>}
|
||
{t.owner && t.due && <span> · </span>}
|
||
{t.due && <span>{t.due}</span>}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</SummaryCard>
|
||
|
||
<SummaryCard icon={Target} title="决议" accent="text-warning">
|
||
<ul className="space-y-2">
|
||
{summary.decisions.map((d, i) => (
|
||
<li key={i} className="flex gap-2 text-[14px] leading-[1.6]">
|
||
<span className="text-success shrink-0">✓</span>
|
||
<span>{d}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</SummaryCard>
|
||
|
||
<SummaryCard icon={Hash} title="关键词" accent="text-info">
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{summary.keywords.map((kw) => (
|
||
<span
|
||
key={kw}
|
||
className="inline-flex items-center rounded-full bg-primary-subtle px-2.5 py-1 text-[12px] font-medium text-primary"
|
||
>
|
||
#{kw}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</SummaryCard>
|
||
</>
|
||
)}
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-1 items-center justify-center p-10">
|
||
<div className="text-center">
|
||
<div className="text-[15px] text-muted-foreground">
|
||
会议正在处理中,状态:{meeting.status}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</AppShell>
|
||
)
|
||
}
|
||
|
||
function SummaryCard({
|
||
icon: Icon,
|
||
title,
|
||
accent,
|
||
children,
|
||
}: {
|
||
icon: any
|
||
title: string
|
||
accent: string
|
||
children: React.ReactNode
|
||
}) {
|
||
return (
|
||
<div className="rounded-2xl border border-border bg-surface-elevated p-5">
|
||
<div className="mb-3 flex items-center gap-2">
|
||
<Icon className={cn('h-4 w-4', accent)} />
|
||
<h3 className="text-[14px] font-semibold">{title}</h3>
|
||
</div>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|