Files
meetnote/web/app/meetings/[id]/page.tsx
2026-04-13 19:00:17 +08:00

257 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}