From 877a4a0654b765df650cfac6b7eddf1ae0f2c17f Mon Sep 17 00:00:00 2001 From: chenhao Date: Fri, 12 Jun 2026 17:22:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E9=A1=B5=E9=9D=A2=E5=92=8C=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `MeetingPreviewView` 组件,用于展示会议预览页面 - 添加 `MeetingPreviewView.css` 样式文件,定义会议预览页面的样式 - 实现会议基本信息、AI 纪要、AI 目录和转录原文的展示 - 支持分享设置、复制链接和章节跳转功能 --- .../components/preview/MeetingPreviewView.css | 561 ++++++++++++++++ .../components/preview/MeetingPreviewView.tsx | 631 ++++++++++++++++++ imeeting-h5/src/utils/meeting.ts | 2 +- 3 files changed, 1193 insertions(+), 1 deletion(-) create mode 100644 imeeting-h5/src/components/preview/MeetingPreviewView.css create mode 100644 imeeting-h5/src/components/preview/MeetingPreviewView.tsx diff --git a/imeeting-h5/src/components/preview/MeetingPreviewView.css b/imeeting-h5/src/components/preview/MeetingPreviewView.css new file mode 100644 index 0000000..0c9c051 --- /dev/null +++ b/imeeting-h5/src/components/preview/MeetingPreviewView.css @@ -0,0 +1,561 @@ +.meeting-preview-page { + --primary-blue: #5f51ff; + --primary-gradient: linear-gradient(135deg, #5f51ff, #6c8cff); + --bg-surface: #ffffff; + --bg-app: #fbfcfd; + --border-color: rgba(228, 232, 245, 0.8); + --text-main: #1a1f36; + --text-secondary: #6e7695; + --card-shadow: 0 10px 30px rgba(127, 139, 186, 0.08); + min-height: 100vh; + background: var(--bg-app); + color: var(--text-main); +} + +.meeting-preview-container { + overflow-y: auto; +} + +.meeting-preview-shell { + max-width: 1000px; + margin: 0 auto; + padding: 32px 20px 120px; +} + +.meeting-preview-top-hero { + display: flex; + gap: 20px; + align-items: flex-start; + margin-bottom: 24px; +} + +.meeting-preview-hero-logo { + width: 56px; + height: 56px; + border-radius: 16px; + background: var(--primary-gradient); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + flex-shrink: 0; +} + +.meeting-preview-hero-title { + margin: 0 0 12px; + font-size: 32px; + font-weight: 800; + line-height: 1.2; +} + +.meeting-preview-status-tag { + padding: 4px 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 700; +} + +.meeting-preview-status-tag.is-complete { + background: #e6f4ea; + color: #1e8e3e; +} + +.meeting-preview-status-tag.is-processing { + background: #e8f0fe; + color: #1a73e8; +} + +.meeting-preview-status-tag.is-warning { + background: #fff4e5; + color: #b76e00; +} + +.meeting-preview-collapsible-section, +.meeting-preview-share-settings, +.meeting-preview-content-card { + background: var(--bg-surface); + border-radius: 20px; + border: 1px solid var(--border-color); + box-shadow: var(--card-shadow); +} + +.meeting-preview-collapsible-section { + margin-bottom: 20px; + overflow: hidden; +} + +.meeting-preview-collapsible-trigger { + padding: 16px 20px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + background: #f8faff; +} + +.meeting-preview-collapsible-trigger .trigger-left { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; +} + +.meeting-preview-collapsible-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.25s ease; +} + +.meeting-preview-collapsible-content.is-expanded { + max-height: 420px; +} + +.meeting-preview-metrics-grid { + padding: 20px; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; +} + +.metric-item { + display: flex; + flex-direction: column; + gap: 8px; +} + +.metric-item-full { + grid-column: 1 / -1; +} + +.metric-label { + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + text-transform: uppercase; +} + +.metric-value { + font-size: 15px; + font-weight: 600; +} + +.metric-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.metric-tag { + padding: 4px 12px; + background: #f0f2ff; + color: var(--primary-blue); + border-radius: 8px; + font-size: 13px; + font-weight: 600; +} + +.meeting-preview-share-settings { + padding: 20px; + margin-bottom: 20px; +} + +.meeting-preview-share-settings-title { + font-size: 18px; + font-weight: 800; + margin-bottom: 4px; +} + +.meeting-preview-share-settings-desc { + color: var(--text-secondary); + margin-bottom: 16px; +} + +.meeting-preview-share-settings-row { + display: flex; + gap: 12px; +} + +.meeting-preview-share-settings-row .ant-input-affix-wrapper { + border-radius: 14px; +} + +.meeting-preview-share-bar { + display: flex; + gap: 16px; + margin-bottom: 24px; +} + +.meeting-preview-share-bar .ant-btn { + flex: 1; +} + +.share-btn-primary, +.share-btn-ghost { + height: 52px !important; + border-radius: 14px !important; + font-weight: 700 !important; +} + +.share-btn-ghost { + border: 1px solid var(--border-color) !important; + background: #fff !important; +} + +.meeting-preview-tabs-container { + padding: 8px 20px 0; + background: #f8faff; + border-bottom: 1px solid var(--border-color); +} + +.meeting-preview-tabs-container .ant-tabs-nav { + margin-bottom: 0; +} + +.meeting-preview-tabs-container .ant-tabs-tab { + padding: 16px 12px; + font-weight: 700; +} + +.meeting-preview-tab-content { + padding: 24px; +} + +.meeting-preview-summary-box { + background: #f8faff; + border: 1px solid #eef1f9; + border-radius: 16px; + padding: 20px; + margin-bottom: 24px; +} + +.meeting-preview-summary-section-title { + color: #9aa0bd; + font-size: 14px; + font-weight: 700; + margin-bottom: 12px; +} + +.meeting-preview-record-tags { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.meeting-preview-record-tag { + min-height: 34px; + padding: 0 14px; + border-radius: 999px; + border: 1px solid #e6e8f5; + background: #fff; + color: #4c5a86; + font-size: 14px; + font-weight: 600; + display: inline-flex; + align-items: center; +} + +.meeting-preview-catalog-list, +.meeting-preview-transcript-list { + display: flex; + flex-direction: column; +} + +.meeting-preview-catalog-item-container { + display: flex; + gap: 16px; + padding: 0 10px; +} + +.meeting-preview-catalog-timeline-axis { + display: flex; + flex-direction: column; + align-items: center; + width: 20px; + flex-shrink: 0; + position: relative; +} + +.meeting-preview-catalog-timeline-dot { + width: 10px; + height: 10px; + background: var(--primary-blue); + border-radius: 50%; + margin-top: 24px; + z-index: 2; +} + +.meeting-preview-catalog-timeline-line { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + background: #eef1f9; +} + +.meeting-preview-catalog-item-card { + flex: 1; + background: #fff; + border: 1px solid var(--border-color); + border-radius: 16px; + padding: 18px 20px; + margin-bottom: 16px; + cursor: pointer; +} + +.meeting-preview-catalog-item-container.active .meeting-preview-catalog-item-card { + border-color: var(--primary-blue); + background: #f9faff; +} + +.meeting-preview-catalog-item-time { + font-family: monospace; + font-size: 13px; + color: var(--primary-blue); + font-weight: 700; + margin-bottom: 6px; +} + +.meeting-preview-catalog-item-title-row { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.meeting-preview-catalog-item-title { + font-weight: 700; + font-size: 16px; +} + +.meeting-preview-catalog-item-link { + padding: 4px 10px; + border-radius: 8px; + border: none; + background: rgba(95, 81, 255, 0.08); + color: var(--primary-blue); + font-size: 12px; + font-weight: 700; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.meeting-preview-section-header { + display: flex; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.meeting-preview-section-title { + font-size: 20px; + font-weight: 800; +} + +.meeting-preview-section-extra { + color: var(--text-secondary); + font-size: 13px; +} + +.meeting-preview-transcript-list { + gap: 18px; + padding-bottom: 120px; +} + +.meeting-preview-transcript-item { + display: flex; + gap: 14px; + padding: 12px; + border-radius: 18px; + border: 1px solid transparent; + cursor: pointer; +} + +.meeting-preview-transcript-item.is-linked { + background: rgba(95, 81, 255, 0.04); + border-color: rgba(95, 81, 255, 0.1); +} + +.meeting-preview-transcript-item.is-active { + background: #fff; + border-color: rgba(95, 81, 255, 0.2); +} + +.meeting-preview-transcript-avatar { + width: 44px; + height: 44px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 800; + font-size: 16px; + flex-shrink: 0; +} + +.meeting-preview-transcript-content { + flex: 1; + min-width: 0; +} + +.meeting-preview-transcript-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.meeting-preview-transcript-speaker { + font-weight: 800; + font-size: 15px; +} + +.meeting-preview-transcript-time { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + font-family: monospace; + background: #f1f3f7; + padding: 2px 8px; + border-radius: 6px; +} + +.meeting-preview-transcript-text { + font-size: 16px; + line-height: 1.8; + color: #3e4766; +} + +.meeting-preview-markdown { + line-height: 1.8; +} + +.meeting-preview-audio-player-inline { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 32px); + max-width: 720px; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(24px); + border: 1px solid rgba(255, 255, 255, 0.4); + display: flex; + align-items: center; + padding: 12px 20px; + border-radius: 24px; + z-index: 1000; + box-shadow: 0 25px 50px -12px rgba(95, 81, 255, 0.25); +} + +.audio-player-content { + width: 100%; + display: flex; + align-items: center; + gap: 14px; +} + +.audio-play-btn { + width: 46px; + height: 46px; + border-radius: 14px; + border: none; + background: var(--primary-gradient); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + cursor: pointer; + flex-shrink: 0; +} + +.audio-progress-container { + flex: 1; + display: flex; + align-items: center; + gap: 10px; +} + +.audio-time { + font-family: "JetBrains Mono", monospace; + font-size: 11px; + font-weight: 700; + color: var(--text-secondary); + width: 44px; + flex-shrink: 0; +} + +.audio-range { + flex: 1; +} + +.audio-speed-btn { + background: #f1f3f7; + border: none; + width: 44px; + height: 36px; + border-radius: 12px; + font-size: 12px; + font-weight: 800; + cursor: pointer; + color: var(--primary-blue); + flex-shrink: 0; +} + +.meeting-preview-footer { + margin-top: 48px; + display: flex; + justify-content: center; +} + +.meeting-preview-disclaimer { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); + font-size: 12px; + background: #f1f3f7; + padding: 8px 18px; + border-radius: 99px; +} + +@media (max-width: 768px) { + .meeting-preview-shell { + padding: 24px 14px 110px; + } + + .meeting-preview-hero-title { + font-size: 24px; + } + + .meeting-preview-metrics-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .meeting-preview-share-bar, + .meeting-preview-share-settings-row, + .meeting-preview-catalog-item-title-row { + flex-direction: column; + } + + .meeting-preview-share-bar .ant-btn, + .meeting-preview-share-settings-row .ant-btn, + .meeting-preview-share-settings-row .ant-input-affix-wrapper { + width: 100%; + } + + .meeting-preview-tab-content { + padding: 18px; + } + + .meeting-preview-transcript-meta { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/imeeting-h5/src/components/preview/MeetingPreviewView.tsx b/imeeting-h5/src/components/preview/MeetingPreviewView.tsx new file mode 100644 index 0000000..78ff32f --- /dev/null +++ b/imeeting-h5/src/components/preview/MeetingPreviewView.tsx @@ -0,0 +1,631 @@ +import {useMemo, useRef, useState} from "react"; +import { + AudioOutlined, + CalendarOutlined, + CaretRightFilled, + ClockCircleOutlined, + CopyOutlined, + DownOutlined, + FileTextOutlined, + LinkOutlined, + LockOutlined, + PauseOutlined, + RobotOutlined, + ShareAltOutlined, + TeamOutlined, + UpOutlined, + UserOutlined, +} from "@ant-design/icons"; +import {Alert, Button, Empty, Input, Tabs, message} from "antd"; +import dayjs from "dayjs"; +import ReactMarkdown from "react-markdown"; + +import {resolveAudioMimeType, resolveMeetingPlaybackAudioUrl} from "@/api/meeting"; +import {buildMeetingAnalysis} from "@/components/preview/meetingAnalysis"; +import type {MeetingChapterVO, MeetingTranscriptVO, MeetingVO} from "@/types"; +import "./MeetingPreviewView.css"; + +type PreviewPageTab = "summary" | "catalog" | "transcript"; + +type ChapterTranscriptLink = { + key: string; + title: string; + timeLabel: string; + transcriptIds: number[]; + firstTranscriptId: number | null; + firstTranscriptStartTime: number | null; +}; + +interface MeetingPreviewViewProps { + meeting: MeetingVO; + transcripts: MeetingTranscriptVO[]; + meetingChapters: MeetingChapterVO[]; + shareUrl: string; + editableShare?: boolean; + sharePasswordDraft?: string; + shareSaving?: boolean; + onSharePasswordDraftChange?: (value: string) => void; + onSaveSharePassword?: () => void; + onCopyShareLink?: () => void | Promise; +} + +const TEXT = { + statusTranscribing: "转写中", + statusSummarizing: "总结中", + statusCompleted: "已完成", + statusPending: "待处理", + pageSummary: "AI 纪要", + pageCatalog: "AI 目录", + pageTranscript: "转录原文", + copyLink: "复制链接", + shareNow: "立即分享", + shareCopied: "预览链接已复制", + shareFallbackCopied: "当前设备不支持系统分享,已为你复制链接", + shareFailed: "分享失败,请先复制链接", + basicInfo: "基本信息", + meetingTime: "会议时间", + hostCreator: "主持/创建", + participantsCount: "参会人数", + tags: "会议标签", + noSummary: "暂无会议纪要", + noCatalog: "暂无 AI 目录", + noTranscript: "暂无转录内容", + noDuration: "暂无时长", + audioUnavailable: "音频文件不可用,仅展示转录内容。", + transcriptTitle: "逐段转录", + keywordSection: "关键词", + linkToTranscript: "关联原文", + shareSettings: "分享访问设置", + shareSettingsHint: "当前登录用户可直接查看,访问密码仅对分享出去的 H5 预览链接生效。", + saveSharePassword: "保存访问密码", + passwordPlaceholder: "为空表示取消访问密码", + disclaimer: "智能内容由用户会议内容与 AI 模型生成,我们不对内容准确性和完整性做任何保证。", +}; + +const STATUS_META: Record = { + 1: {label: TEXT.statusTranscribing, className: "is-processing"}, + 2: {label: TEXT.statusSummarizing, className: "is-processing"}, + 3: {label: TEXT.statusCompleted, className: "is-complete"}, +}; + +function parseChapterTimeToMs(value?: string) { + const raw = String(value || "").trim(); + if (!raw) return null; + const matched = raw.match(/(\d{1,2}:\d{2}(?::\d{2})?)/)?.[1]; + if (!matched) return null; + const parts = matched.split(":").map((item) => Number(item)); + if (parts.some((item) => Number.isNaN(item))) return null; + const totalSeconds = + parts.length === 3 ? parts[0] * 3600 + parts[1] * 60 + parts[2] : parts[0] * 60 + parts[1]; + return totalSeconds * 1000; +} + +function splitDisplayItems(value?: string) { + return (value || "") + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function transcriptColorSeed(speakerKey: string) { + const palette = ["#315f8b", "#b86432", "#557a46", "#6d4fa7", "#a33f57", "#0f766e"]; + const score = Array.from(speakerKey).reduce((sum, char) => sum + char.charCodeAt(0), 0); + return palette[score % palette.length]; +} + +async function copyText(text: string) { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", "true"); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); +} + +function formatDurationRange(startTime?: number, endTime?: number) { + const format = (milliseconds?: number) => { + const safeMs = Math.max(0, milliseconds || 0); + const totalSeconds = Math.floor(safeMs / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + }; + return `${format(startTime)} - ${format(endTime)}`; +} + +function formatTotalDuration(ms: number) { + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if (hours > 0) { + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + } + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; +} + +export default function MeetingPreviewView({ + meeting, + transcripts, + meetingChapters, + shareUrl, + editableShare = false, + sharePasswordDraft = "", + shareSaving = false, + onSharePasswordDraftChange, + onSaveSharePassword, + onCopyShareLink, + }: MeetingPreviewViewProps) { + const audioRef = useRef(null); + const transcriptItemRefs = useRef>({}); + const [pageTab, setPageTab] = useState("summary"); + const [activeTranscriptId, setActiveTranscriptId] = useState(null); + const [audioPlaying, setAudioPlaying] = useState(false); + const [audioCurrentTime, setAudioCurrentTime] = useState(0); + const [audioDuration, setAudioDuration] = useState(0); + const [audioPlaybackRate, setAudioPlaybackRate] = useState(1); + const [isMetricsExpanded, setIsMetricsExpanded] = useState(false); + const [linkedTranscriptIds, setLinkedTranscriptIds] = useState([]); + const [linkedChapterKey, setLinkedChapterKey] = useState(null); + + const analysis = useMemo( + () => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ""), + [meeting?.analysis, meeting?.summaryContent, meeting?.tags], + ); + const participants = useMemo(() => splitDisplayItems(meeting?.participants), [meeting?.participants]); + const tags = useMemo(() => splitDisplayItems(meeting?.tags), [meeting?.tags]); + const playbackAudioUrl = useMemo(() => resolveMeetingPlaybackAudioUrl(meeting), [meeting]); + const statusMeta = STATUS_META[meeting?.status || 0] || { + label: TEXT.statusPending, + className: "is-warning", + }; + + const meetingDuration = useMemo(() => { + if (transcripts.length > 0) { + return transcripts[transcripts.length - 1]?.endTime || 0; + } + return meeting.duration || 0; + }, [meeting.duration, transcripts]); + + const catalogChapterLinks = useMemo(() => { + const transcriptIdToIndex = new Map(transcripts.map((item, index) => [item.id, index])); + const sourceChapters: MeetingChapterVO[] = meetingChapters.length + ? meetingChapters + : analysis.chapters.map((item) => ({ + title: item.title, + time: item.time, + })); + + return sourceChapters.map((chapter, index) => { + let matchedTranscripts: MeetingTranscriptVO[] = []; + const sourceTranscriptIds = Array.isArray(chapter.sourceTranscriptIds) + ? chapter.sourceTranscriptIds + .map((item: number) => Number(item)) + .filter((item: number) => Number.isFinite(item) && transcriptIdToIndex.has(item)) + : []; + + if (sourceTranscriptIds.length) { + matchedTranscripts = sourceTranscriptIds + .map((item: number) => transcripts[transcriptIdToIndex.get(item)!]) + .filter(Boolean); + } else if (chapter.startTranscriptId && chapter.endTranscriptId) { + const startIndex = transcriptIdToIndex.get(Number(chapter.startTranscriptId)); + const endIndex = transcriptIdToIndex.get(Number(chapter.endTranscriptId)); + if (startIndex !== undefined && endIndex !== undefined) { + matchedTranscripts = transcripts.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex) + 1); + } + } else { + const startMs = typeof chapter.startTime === "number" ? chapter.startTime : parseChapterTimeToMs(chapter.time); + const nextChapterStartMs = sourceChapters + .slice(index + 1) + .map((item) => (typeof item.startTime === "number" ? item.startTime : parseChapterTimeToMs(item.time))) + .find((item): item is number => item !== null && startMs !== null && item > startMs); + + if (startMs !== null) { + const firstTranscriptIndex = transcripts.findIndex((item) => (item.endTime || 0) > startMs); + if (firstTranscriptIndex >= 0) { + const lastTranscriptIndex = + nextChapterStartMs === undefined + ? transcripts.length + : transcripts.findIndex((item) => (item.startTime || 0) >= nextChapterStartMs); + matchedTranscripts = transcripts.slice( + firstTranscriptIndex, + lastTranscriptIndex >= 0 ? lastTranscriptIndex : transcripts.length, + ); + } + } + } + + return { + key: `${chapter.chapterNo ?? index}-${chapter.title || "chapter"}`, + title: chapter.title || `章节 ${index + 1}`, + timeLabel: chapter.time || "--:--", + transcriptIds: matchedTranscripts.map((item) => item.id), + firstTranscriptId: matchedTranscripts[0]?.id ?? null, + firstTranscriptStartTime: matchedTranscripts[0]?.startTime ?? null, + }; + }); + }, [analysis.chapters, meetingChapters, transcripts]); + + const handleTranscriptSeek = (item: MeetingTranscriptVO) => { + if (!audioRef.current) return; + audioRef.current.currentTime = Math.max(0, (item.startTime || 0) / 1000); + void audioRef.current.play().catch(() => { + }); + }; + + const handleLocateChapterTranscript = (index: number) => { + const link = catalogChapterLinks[index]; + if (!link || !link.firstTranscriptId) return; + + setPageTab("transcript"); + setLinkedTranscriptIds(link.transcriptIds); + setLinkedChapterKey(link.key); + setActiveTranscriptId(link.firstTranscriptId); + + const target = transcriptItemRefs.current[link.firstTranscriptId]; + target?.scrollIntoView({behavior: "smooth", block: "center"}); + + if (audioRef.current && link.firstTranscriptStartTime !== null) { + audioRef.current.currentTime = Math.max(0, link.firstTranscriptStartTime / 1000); + void audioRef.current.play().catch(() => { + }); + } + }; + + const handleAudioTimeUpdate = () => { + if (!audioRef.current) return; + const currentSeconds = audioRef.current.currentTime; + setAudioCurrentTime(currentSeconds); + if (audioRef.current.duration && audioDuration !== audioRef.current.duration) { + setAudioDuration(audioRef.current.duration); + } + const currentMs = currentSeconds * 1000; + const currentItem = transcripts.find( + (item) => currentMs >= (item.startTime || 0) && currentMs <= (item.endTime || 0), + ); + setActiveTranscriptId(currentItem?.id || null); + }; + + const handleCopyLink = async () => { + if (onCopyShareLink) { + await onCopyShareLink(); + return; + } + try { + await copyText(shareUrl); + message.success(TEXT.shareCopied); + } catch { + message.error(TEXT.shareFailed); + } + }; + + const handleShareNow = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: meeting?.title || "会议预览", + text: "我向你分享了一个会议预览链接", + url: shareUrl, + }); + return; + } + await copyText(shareUrl); + message.success(TEXT.shareFallbackCopied); + } catch { + message.error(TEXT.shareFailed); + } + }; + + return ( +
+
+
+
+
+ +
+
+

{meeting.title || "未命名会议"}

+
+ {statusMeta.label} +
+
+
+ +
+
setIsMetricsExpanded((value) => !value)}> +
+ + {TEXT.basicInfo} +
+
{isMetricsExpanded ? : }
+
+ +
+
+
+
{TEXT.meetingTime}
+
+ + {meeting.meetingTime ? dayjs(meeting.meetingTime).format("YYYY-MM-DD HH:mm") : "未设置"} +
+
+
+
{TEXT.hostCreator}
+
+ + {meeting.creatorName || "未设置"} +
+
+
+
{TEXT.participantsCount}
+
+ + {participants.length} 人 +
+
+
+
会议时长
+
+ + {meetingDuration > 0 ? formatTotalDuration(meetingDuration) : "未设置"} +
+
+ {tags.length > 0 ? ( +
+
{TEXT.tags}
+
+ {tags.map((tag) => ( + + #{tag} + + ))} +
+
+ ) : null} +
+
+
+ + {editableShare ? ( +
+
{TEXT.shareSettings}
+
{TEXT.shareSettingsHint}
+
+ } + onChange={(event) => onSharePasswordDraftChange?.(event.target.value)} + /> + +
+
+ ) : null} + +
+ + +
+ +
+
+ setPageTab(key as PreviewPageTab)} + items={[ + {key: "summary", label: TEXT.pageSummary}, + {key: "catalog", label: TEXT.pageCatalog}, + {key: "transcript", label: TEXT.pageTranscript}, + ]} + /> +
+ +
+ {pageTab === "summary" ? ( + <> +
+
{TEXT.keywordSection}
+
+ {analysis.keywords.length ? ( + analysis.keywords.map((item) => ( +
+ #{item} +
+ )) + ) : ( + 暂无关键词 + )} +
+
+
+ {meeting.summaryContent ? {meeting.summaryContent} : + } +
+ + ) : null} + + {pageTab === "catalog" ? ( +
+ {catalogChapterLinks.length ? ( + catalogChapterLinks.map((chapter, index) => ( +
+
+
+
+
+
handleLocateChapterTranscript(index)}> +
{chapter.timeLabel}
+
+
{chapter.title}
+ +
+
+
+ )) + ) : ( + + )} +
+ ) : null} + + {pageTab === "transcript" ? ( + <> +
+
{TEXT.transcriptTitle}
+
+ + {meetingDuration > 0 ? formatDurationRange(0, meetingDuration) : TEXT.noDuration} +
+
+ + {meeting.audioSaveStatus === "FAILED" ? ( + + ) : null} + +
+ {transcripts.length ? ( + transcripts.map((item) => { + const speakerKey = item.speakerName || item.speakerLabel || item.speakerId || "speaker"; + const isLinked = linkedTranscriptIds.includes(item.id); + const isActive = activeTranscriptId === item.id; + return ( +
{ + transcriptItemRefs.current[item.id] = node; + }} + className={`meeting-preview-transcript-item ${isActive ? "is-active" : ""} ${isLinked ? "is-linked" : ""}`} + onClick={() => { + handleTranscriptSeek(item); + setLinkedTranscriptIds([]); + setLinkedChapterKey(null); + }} + > +
+ {speakerKey.slice(0, 1)} +
+
+
+ {speakerKey} + {formatDurationRange(item.startTime, item.endTime)} +
+
{item.content || TEXT.noTranscript}
+
+
+ ); + }) + ) : ( + + )} +
+ + ) : null} +
+
+ +
+
+ + {TEXT.disclaimer} +
+
+
+
+ + {playbackAudioUrl ? ( +
+ +
+ +
+
{formatTotalDuration(audioCurrentTime * 1000)}
+ { + if (!audioRef.current) return; + const time = parseFloat(e.target.value); + audioRef.current.currentTime = time; + setAudioCurrentTime(time); + }} + /> +
{formatTotalDuration(audioDuration * 1000)}
+
+ +
+
+ ) : null} +
+ ); +} diff --git a/imeeting-h5/src/utils/meeting.ts b/imeeting-h5/src/utils/meeting.ts index a7e3090..a63d6ec 100644 --- a/imeeting-h5/src/utils/meeting.ts +++ b/imeeting-h5/src/utils/meeting.ts @@ -47,7 +47,7 @@ export function getSummarySnippet(meeting: Pick