From beab0edabefc1d59894ac3f471131b825f6cd30c Mon Sep 17 00:00:00 2001 From: chenhao Date: Sat, 9 May 2026 17:33:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E7=AB=A0=E8=8A=82=E5=92=8C=E7=9B=AE=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `PublicMeetingPreviewVO` 中添加 `chapters` 字段 - 更新 `MeetingPreview` 组件以支持章节和目录展示 - 添加新的 `pageCatalog` 选项卡,展示 AI 生成的目录 - 实现章节与转录内容的关联和跳转功能 - 优化关键词和总结内容的展示布局 --- .../biz/MeetingPublicPreviewController.java | 1 + .../dto/biz/PublicMeetingPreviewVO.java | 2 + frontend/src/api/business/meeting.ts | 1 + .../src/pages/business/MeetingPreview.css | 1378 ++++++++--------- .../src/pages/business/MeetingPreview.tsx | 815 +++++----- 5 files changed, 1128 insertions(+), 1069 deletions(-) diff --git a/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java b/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java index a37a4bb..439cae9 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/MeetingPublicPreviewController.java @@ -53,6 +53,7 @@ public class MeetingPublicPreviewController { data.getMeeting().setAccessPassword(null); } data.setTranscripts(meetingQueryService.getTranscripts(id)); + data.setChapters(meetingQueryService.getChapters(id)); return ApiResponse.ok(data); } catch (RuntimeException ex) { return ApiResponse.error(ex.getMessage()); diff --git a/backend/src/main/java/com/imeeting/dto/biz/PublicMeetingPreviewVO.java b/backend/src/main/java/com/imeeting/dto/biz/PublicMeetingPreviewVO.java index 6e4e09e..019c475 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/PublicMeetingPreviewVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/PublicMeetingPreviewVO.java @@ -3,9 +3,11 @@ package com.imeeting.dto.biz; import lombok.Data; import java.util.List; +import java.util.Map; @Data public class PublicMeetingPreviewVO { private MeetingVO meeting; private List transcripts; + private List> chapters; } diff --git a/frontend/src/api/business/meeting.ts b/frontend/src/api/business/meeting.ts index 831fd07..684dfdf 100644 --- a/frontend/src/api/business/meeting.ts +++ b/frontend/src/api/business/meeting.ts @@ -266,6 +266,7 @@ export interface MeetingChapterVO { export interface PublicMeetingPreviewVO { meeting: MeetingVO; transcripts: MeetingTranscriptVO[]; + chapters?: MeetingChapterVO[]; } export const getMeetingDetail = (id: number) => { diff --git a/frontend/src/pages/business/MeetingPreview.css b/frontend/src/pages/business/MeetingPreview.css index 8ae43b9..790c398 100644 --- a/frontend/src/pages/business/MeetingPreview.css +++ b/frontend/src/pages/business/MeetingPreview.css @@ -1,490 +1,737 @@ .meeting-preview-page { - --preview-bg: var(--app-bg-main); - --preview-ink: var(--app-text-main); - --preview-muted: var(--app-text-secondary); - --preview-line: var(--app-border-color); - --preview-card: var(--app-bg-card); - --preview-card-strong: var(--app-bg-surface-soft); - --preview-shadow: var(--app-shadow-lg); - --preview-accent: var(--app-primary-color); - --preview-accent-soft: rgba(22, 119, 255, 0.1); - --preview-cool: #1677ff; - position: relative; - min-height: 100vh; - padding: 24px 16px 40px; - background: var(--preview-bg); - color: var(--preview-ink); + --header-height: 64px; + --audio-player-height: 80px; + --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); + + height: 100vh; + display: flex; + flex-direction: column; + background: var(--bg-app); + color: var(--text-main); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; overflow: hidden; } -.meeting-preview-page::before, -.meeting-preview-page::after { - content: ""; - position: fixed; - pointer-events: none; - z-index: 0; +/* Glassmorphism Header - Now redundant but kept for safety if needed */ +.meeting-preview-header { + display: none; } -.meeting-preview-page::before { - top: 64px; - right: -54px; - width: 180px; - height: 180px; - border-radius: 50%; - background: - radial-gradient(circle, rgba(255, 255, 255, 0.84) 0%, rgba(255, 255, 255, 0) 72%), - radial-gradient(circle at 36% 36%, rgba(22, 119, 255, 0.18), rgba(22, 119, 255, 0) 56%); - filter: blur(2px); -} - -.meeting-preview-page::after { - left: -48px; - bottom: 120px; - width: 160px; - height: 160px; - border-radius: 32px; - border: 1px solid rgba(22, 119, 255, 0.14); - background: - linear-gradient(135deg, rgba(22, 119, 255, 0.12), rgba(22, 119, 255, 0)), - rgba(255, 255, 255, 0.32); - transform: rotate(16deg); +/* Main Container */ +.meeting-preview-container { + flex: 1; + overflow-y: auto; + position: relative; } .meeting-preview-shell { - position: relative; - z-index: 1; - width: min(100%, 920px); + max-width: 1000px; margin: 0 auto; + padding: 48px 24px; } -.meeting-preview-loading, -.meeting-preview-empty { - display: grid; - gap: 18px; +/* Hero Section */ +.meeting-preview-top-hero { + display: flex; + gap: 24px; + align-items: flex-start; + margin-bottom: 32px; } -.meeting-preview-card { - position: relative; - overflow: hidden; - border: 1px solid var(--preview-line); - border-radius: 24px; - background: var(--preview-card); - box-shadow: var(--preview-shadow); -} - -.meeting-preview-hero, -.meeting-preview-section { - position: relative; - z-index: 1; - animation: preview-rise 0.5s ease both; -} - -.meeting-preview-hero { - padding: 24px 20px 22px; -} - -.meeting-preview-eyebrow { +.meeting-preview-hero-logo { + width: 56px; + height: 56px; + border-radius: 16px; + background: var(--primary-gradient); + color: white; display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 16px; + justify-content: center; + font-size: 28px; + box-shadow: 0 12px 24px rgba(95, 81, 255, 0.25); + flex-shrink: 0; } -.meeting-preview-eyebrow-label { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-radius: 999px; - background: var(--app-bg-surface); - border: 1px solid var(--preview-line); - color: var(--preview-muted); - font-size: 12px; - letter-spacing: 0.12em; - text-transform: uppercase; +.meeting-preview-hero-content { + flex: 1; } -.meeting-preview-status { - display: inline-flex; - align-items: center; - padding: 7px 12px; - border-radius: 999px; - font-size: 12px; - font-weight: 600; - letter-spacing: 0.04em; -} - -.meeting-preview-status.is-complete { - background: rgba(82, 196, 26, 0.12); - color: #52c41a; -} - -.meeting-preview-status.is-processing { - background: rgba(22, 119, 255, 0.12); - color: #1677ff; -} - -.meeting-preview-status.is-warning { - background: rgba(250, 173, 20, 0.12); - color: #faad14; -} - -.meeting-preview-title { - margin: 0; - font-family: Georgia, "Times New Roman", "Songti SC", serif; - font-size: clamp(32px, 8vw, 46px); - line-height: 1.02; - letter-spacing: -0.04em; -} - -.meeting-preview-title-number { - font-family: "Segoe UI", "SF Pro Display", "Helvetica Neue", sans-serif; - font-variant-numeric: lining-nums proportional-nums; +.meeting-preview-hero-title { + margin: 0 0 12px; + font-size: 32px; + font-weight: 800; + color: var(--text-main); letter-spacing: -0.02em; + line-height: 1.2; } -.meeting-preview-hero-toolbar { - display: grid; - gap: 18px; -} - -.meeting-preview-hero-actions { +.meeting-preview-hero-meta { display: flex; - flex-wrap: wrap; + align-items: center; + gap: 16px; +} + +.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-hero-id { + font-size: 13px; + color: var(--text-secondary); + font-family: monospace; +} + +/* Collapsible Info */ +.meeting-preview-collapsible-section { + background: var(--bg-surface); + border-radius: 20px; + border: 1px solid var(--border-color); + margin-bottom: 24px; + overflow: hidden; + transition: all 0.3s ease; +} + +.meeting-preview-collapsible-trigger { + padding: 16px 24px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + background: #f8faff; + transition: background 0.2s; +} + +.meeting-preview-collapsible-trigger:hover { + background: #f0f4ff; +} + +.meeting-preview-collapsible-trigger .trigger-left { + display: flex; + align-items: center; gap: 12px; + font-weight: 700; + color: #435285; } -.meeting-preview-subtitle { - margin: 14px 0 0; - color: var(--preview-muted); - font-size: 15px; - line-height: 1.75; +.meeting-preview-collapsible-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s cubic-bezier(0.16, 1, 0.3, 1); } -.meeting-preview-metrics { +.meeting-preview-collapsible-content.is-expanded { + max-height: 400px; +} + +.meeting-preview-metrics-grid { + padding: 24px; display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin-top: 22px; + grid-template-columns: repeat(4, 1fr); + gap: 24px; } -@media (max-width: 768px) { - .meeting-preview-metrics { - grid-template-columns: repeat(2, minmax(0, 1fr)); +@media (max-width: 992px) { + .meeting-preview-metrics-grid { + grid-template-columns: repeat(2, 1fr); } } -.meeting-preview-metric { - padding: 14px 14px 12px; - border-radius: 18px; - background: var(--preview-card-strong); - border: 1px solid var(--preview-line); +@media (max-width: 480px) { + .meeting-preview-metrics-grid { + grid-template-columns: 1fr; + } } -.meeting-preview-metric-label { - display: block; - margin-bottom: 6px; - color: var(--preview-muted); - font-size: 12px; -} - -.meeting-preview-metric-value { - display: block; - font-size: 15px; - font-weight: 600; - line-height: 1.6; - word-break: break-word; -} - -.meeting-preview-panels { - display: grid; - gap: 18px; - margin-top: 18px; -} - -.meeting-preview-page-tabs .ant-tabs-nav { - margin-bottom: 18px; -} - -.meeting-preview-page-tabs .ant-tabs-tab { - font-weight: 600; -} - -.meeting-preview-tab-panel { - display: grid; - gap: 18px; -} - -.meeting-preview-section { - padding: 22px 18px; -} - -.meeting-preview-password-gate { - max-width: 480px; - margin: 10vh auto; - padding: 40px 32px; - text-align: center; - border-radius: 24px; - background: var(--preview-card); - border: 1px solid var(--preview-line); - box-shadow: var(--preview-shadow); - backdrop-filter: blur(24px); -} - -.meeting-preview-password-gate .meeting-preview-section-header { - justify-content: center; - margin-bottom: 12px; -} - -.meeting-preview-password-gate .meeting-preview-section-kicker { - justify-content: center; - margin-bottom: 8px; -} - -.meeting-preview-password-gate .meeting-preview-section-title { - font-size: 28px; -} - -.meeting-preview-password-form { +.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; + letter-spacing: 0.05em; +} + +.metric-value { + font-size: 15px; + font-weight: 600; + color: var(--text-main); +} + +.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; +} + +/* Share Bar */ +.meeting-preview-share-bar { + display: flex; gap: 16px; - margin-top: 28px; + margin-bottom: 40px; +} + +.meeting-preview-share-bar .ant-btn { + flex: 1; +} + +.share-btn-primary { + height: 52px !important; + border-radius: 14px !important; + padding: 0 32px !important; + font-weight: 700 !important; + box-shadow: 0 8px 20px rgba(95, 81, 255, 0.2) !important; +} + +.share-btn-ghost { + height: 52px !important; + border-radius: 14px !important; + padding: 0 24px !important; + font-weight: 700 !important; + border: 1px solid var(--border-color) !important; + background: white !important; +} + +/* Tabs and Content */ +.meeting-preview-content-card { + background: var(--bg-surface); + border-radius: 24px; + border: 1px solid var(--border-color); + box-shadow: var(--card-shadow); + min-height: 600px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.meeting-preview-tabs-container { + padding: 8px 24px 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; + font-size: 16px; +} + +.meeting-preview-tab-content { + padding: 32px; + flex: 1; +} + +.meeting-preview-summary-box { + background: #f8faff; + border: 1px solid #eef1f9; + border-radius: 16px; + padding: 24px; margin-bottom: 24px; } -.meeting-preview-password-form .ant-input-affix-wrapper { - padding: 10px 16px; - border-radius: 12px; - background: var(--app-bg-surface-soft); - border-color: var(--preview-line); +.meeting-preview-summary-section-title { + color: #9aa0bd; + font-size: 14px; + font-weight: 700; + margin-bottom: 12px; + letter-spacing: 0.02em; } -.meeting-preview-password-form .ant-input-affix-wrapper-focused { - border-color: var(--preview-accent); - box-shadow: 0 0 0 2px var(--preview-accent-soft); - background: var(--app-bg-surface); +.meeting-preview-record-tags { + display: flex; + flex-wrap: wrap; + gap: 12px; } -.meeting-preview-password-form .ant-input { - background: transparent; - font-size: 16px; - letter-spacing: 2px; -} - -.meeting-preview-password-form .ant-btn { - height: 48px; - border-radius: 12px; - font-size: 16px; +.meeting-preview-record-tag { + min-height: 34px; + padding: 0 14px; + border-radius: 999px; + border: 1px solid #e6e8f5; + background: #ffffff; + color: #4c5a86; + font-size: 14px; font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(95, 81, 255, 0.04); } -.meeting-preview-section-header { +.meeting-preview-keywords-empty { + color: var(--text-secondary); + font-size: 14px; +} + +/* Catalog List & Timeline */ +.meeting-preview-catalog-list { + display: flex; + flex-direction: column; + padding: 10px 0 20px; +} + +.meeting-preview-catalog-item-container { + display: flex; + gap: 20px; + padding: 0 10px; +} + +.meeting-preview-catalog-item-container.active .meeting-preview-catalog-item-card { + border-color: var(--primary-blue); + background: #f9faff; + box-shadow: 0 8px 20px rgba(95, 81, 255, 0.1); +} + +.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; + transition: all 0.3s ease; + box-shadow: 0 0 0 4px rgba(95, 81, 255, 0.1); +} + +.meeting-preview-catalog-timeline-line { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + background: #eef1f9; + z-index: 1; +} + +.meeting-preview-catalog-item-container:first-child .meeting-preview-catalog-timeline-line { + top: 24px; +} + +.meeting-preview-catalog-item-container:last-child .meeting-preview-catalog-timeline-line { + bottom: calc(100% - 34px); +} + +.meeting-preview-catalog-item-card { + flex: 1; + background: #ffffff; + border: 1px solid var(--border-color); + border-radius: 16px; + padding: 18px 24px; + margin-bottom: 20px; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + cursor: pointer; +} + +.meeting-preview-catalog-item-card:hover { + background: #f9faff; + border-color: rgba(95, 81, 255, 0.2); + transform: translateX(4px); +} + +.meeting-preview-catalog-item-time { + font-family: monospace; + font-size: 13px; + color: var(--primary-blue); + font-weight: 700; + margin-bottom: 4px; +} + +.meeting-preview-catalog-item-title-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; +} + +.meeting-preview-catalog-item-title { + font-weight: 700; + font-size: 16px; + color: var(--text-main); + line-height: 1.4; +} + +.meeting-preview-catalog-item-link { + opacity: 0; + visibility: hidden; + padding: 4px 12px; + border-radius: 8px; + border: 1px solid transparent; + background: rgba(95, 81, 255, 0.08); + color: var(--primary-blue); + font-size: 12px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + white-space: nowrap; +} + +.meeting-preview-catalog-item-container:hover .meeting-preview-catalog-item-link { + opacity: 1; + visibility: visible; +} + +.meeting-preview-catalog-item-link:hover { + background: rgba(95, 81, 255, 0.15); + transform: translateY(-1px); +} + +/* --- Transcription Original Styles Overhaul --- */ +.meeting-preview-transcript-list { + display: flex; + flex-direction: column; + gap: 24px; + padding-bottom: 140px; +} + +.meeting-preview-transcript-item { + display: flex; + gap: 16px; + padding: 12px; + border-radius: 20px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + position: relative; + border: 1px solid transparent; +} + +.meeting-preview-transcript-item:hover { + background: rgba(255, 255, 255, 0.8); + border-color: rgba(95, 81, 255, 0.1); + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(95, 81, 255, 0.04); +} + +.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: #ffffff; + border-color: rgba(95, 81, 255, 0.2); + box-shadow: 0 12px 24px rgba(95, 81, 255, 0.08); +} + +.meeting-preview-transcript-avatar { + width: 44px; + height: 44px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 800; + font-size: 16px; + flex-shrink: 0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + position: sticky; + top: 12px; +} + +.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: 16px; + margin-bottom: 8px; } -.meeting-preview-section-kicker { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--preview-muted); +.meeting-preview-transcript-speaker { + font-weight: 800; + font-size: 15px; + color: var(--text-main); + letter-spacing: -0.01em; +} + +.meeting-preview-transcript-time { font-size: 12px; - letter-spacing: 0.12em; - text-transform: uppercase; + font-weight: 600; + color: var(--text-secondary); + font-family: monospace; + background: #f1f3f7; + padding: 2px 8px; + border-radius: 6px; } -.meeting-preview-section-title { - margin: 6px 0 0; - font-family: Georgia, "Times New Roman", "Songti SC", serif; - font-size: 22px; - letter-spacing: -0.03em; +.meeting-preview-transcript-text { + font-size: 16px; + line-height: 1.8; + color: #3e4766; + font-weight: 500; + word-wrap: break-word; } -.meeting-preview-section-extra { - font-size: 12px; - color: var(--preview-muted); -} - -.meeting-preview-alert { - margin-bottom: 16px; - border-radius: 18px !important; -} - -.meeting-preview-tags { +/* --- Floating Audio Player Overhaul --- */ +.meeting-preview-audio-player-inline { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 32px); + max-width: 720px; + height: auto; + min-height: 80px; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(24px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.4); display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.meeting-preview-tag { - display: inline-flex; align-items: center; - padding: 8px 12px; - border-radius: 999px; - background: var(--preview-card-strong); - border: 1px solid var(--preview-line); - color: var(--preview-ink); - font-size: 13px; + padding: 12px 24px; + border-radius: 28px; + z-index: 1000; + box-shadow: + 0 25px 50px -12px rgba(95, 81, 255, 0.25), + 0 0 0 1px rgba(95, 81, 255, 0.05); } -.meeting-preview-overview { - margin-top: 16px; - padding: 18px; - border-radius: 22px; - background: var(--app-bg-surface-soft); - border: 1px solid var(--preview-line); +.audio-player-content { + width: 100%; + display: flex; + align-items: center; + gap: 16px; + flex-wrap: nowrap; +} + +.audio-play-btn { + width: 48px; + height: 48px; + border-radius: 16px; + border: none; + background: var(--primary-gradient); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + cursor: pointer; + flex-shrink: 0; + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.audio-play-btn:hover { + transform: scale(1.05); + box-shadow: 0 8px 15px rgba(95, 81, 255, 0.3); +} + +.audio-progress-container { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.audio-time { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 700; + color: var(--text-secondary); + width: 40px; + flex-shrink: 0; +} + +.audio-range { + flex: 1; + appearance: none; + height: 6px; + background: #eef1f9; + border-radius: 3px; + cursor: pointer; + outline: none; + overflow: hidden; +} + +.audio-range::-webkit-slider-thumb { + appearance: none; + width: 0; + box-shadow: -100vw 0 0 100vw var(--primary-blue); +} + +.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; + transition: all 0.2s; +} + +.audio-speed-btn:hover { + background: #eef1ff; +} + +/* --- Mobile Optimizations --- */ +@media (max-width: 768px) { + .meeting-preview-transcript-list { + gap: 16px; + } + + .meeting-preview-transcript-item { + gap: 12px; + padding: 10px; + } + + .meeting-preview-transcript-avatar { + width: 36px; + height: 36px; + border-radius: 10px; + font-size: 14px; + } + + .meeting-preview-transcript-text { + font-size: 15px; + } + + /* Fix audio player deformation on mobile */ + .meeting-preview-audio-player-inline { + padding: 10px 16px; + bottom: 16px; + border-radius: 22px; + } + + .audio-player-content { + gap: 10px; + } + + .audio-play-btn { + width: 40px; + height: 40px; + border-radius: 12px; + font-size: 18px; + } + + .audio-time { + display: block; + font-size: 10px; + } + + .audio-progress-container { + gap: 0; + } + + .audio-speed-btn { + width: 38px; + height: 32px; + font-size: 11px; + } +} + +/* Utils */ +.meeting-preview-footer { + margin-top: 64px; + padding-bottom: 64px; + display: flex; + justify-content: center; } .meeting-preview-disclaimer { display: flex; align-items: center; - justify-content: center; - margin-top: 32px; - padding: 16px; - color: var(--app-text-secondary); - font-size: 13px; - text-align: center; -} - -.meeting-preview-overview-copy { - margin: 0; - font-size: 15px; - line-height: 1.9; - color: var(--preview-ink); - white-space: pre-wrap; -} - -.meeting-preview-analysis-tabs { - display: flex; - align-items: center; - background: var(--app-bg-surface-soft); - padding: 5px; - border-radius: 12px; - gap: 4px; - margin: 24px 0 20px; - overflow-x: auto; - scrollbar-width: none; - border: 1px solid var(--app-border-color); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02); - width: 100%; -} - -.meeting-preview-analysis-tabs::-webkit-scrollbar { - display: none; -} - -.meeting-preview-analysis-tab { - flex: 1; - text-align: center; - padding: 8px 12px; - border-radius: 8px; - color: var(--app-text-secondary); - font-size: 14px; - font-weight: 500; - cursor: pointer; - white-space: nowrap; - border: 1px solid transparent; - transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); -} - -.meeting-preview-analysis-tab:hover { - color: var(--app-text-main); - background: rgba(255, 255, 255, 0.4); -} - -.meeting-preview-analysis-tab.active { - background: var(--app-bg-surface); - color: var(--app-primary-color); - font-weight: 600; - border-color: rgba(0, 0, 0, 0.04); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); -} - -.meeting-preview-analysis-tab.active:hover { - background: var(--app-bg-surface); - color: var(--app-primary-color); -} - -.meeting-preview-analysis-panel { - display: grid; - gap: 12px; -} - -.meeting-preview-list-empty { - padding: 26px 16px 10px; -} - -.meeting-preview-chapter, -.meeting-preview-keypoint, -.meeting-preview-speaker-card, -.meeting-preview-todo { - border: 1px solid var(--preview-line); - background: var(--preview-card-strong); - border-radius: 22px; -} - -.meeting-preview-chapter, -.meeting-preview-keypoint { - display: flex; - align-items: flex-start; - gap: 16px; - padding: 20px; -} - -.meeting-preview-chapter-time, -.meeting-preview-keypoint-index { - display: flex; - align-items: center; - justify-content: center; - width: 52px; - height: 52px; - flex-shrink: 0; - border-radius: 14px; - background: var(--preview-accent-soft); - color: var(--preview-accent); - font-weight: 700; - font-size: 16px; -} - -.meeting-preview-item-title { - display: block; - margin-bottom: 6px; - font-size: 15px; - font-weight: 700; - line-height: 1.5; -} - -.meeting-preview-item-copy { - display: block; - color: var(--preview-muted); - font-size: 14px; - line-height: 1.8; -} - -.meeting-preview-item-meta { - display: flex; - flex-wrap: wrap; gap: 8px; - margin-top: 10px; + color: var(--text-secondary); + font-size: 12px; + background: #f1f3f7; + padding: 8px 20px; + border-radius: 99px; } -.meeting-preview-meta-pill { - display: inline-flex; - align-items: center; - padding: 6px 10px; - border-radius: 999px; - background: var(--preview-accent-soft); - color: var(--preview-accent); - font-size: 12px; +@media (max-width: 768px) { + .meeting-preview-shell { padding: 32px 16px; } + .meeting-preview-hero-title { font-size: 24px; } + .meeting-preview-metrics-grid { grid-template-columns: 1fr; gap: 16px; } + .meeting-preview-tabs-container { + padding: 8px 12px 0; + } + .meeting-preview-tabs-container .ant-tabs-nav::before { + inset-inline: 0; + } + .meeting-preview-tabs-container .ant-tabs-nav-list { + width: 100%; + display: grid !important; + grid-template-columns: repeat(3, minmax(0, 1fr)); + transform: none !important; + } + .meeting-preview-tabs-container .ant-tabs-tab { + margin: 0 !important; + padding: 14px 8px !important; + justify-content: center; + text-align: center; + min-width: 0; + } + .meeting-preview-tabs-container .ant-tabs-tab .ant-tabs-tab-btn { + width: 100%; + font-size: 14px; + line-height: 1.2; + white-space: nowrap; + overflow: visible; + text-overflow: clip; + } + .meeting-preview-tabs-container .ant-tabs-nav-operations { + display: none !important; + } + .meeting-preview-tab-content { padding: 20px; } } .meeting-preview-speaker-card { - padding: 16px; + background: #f8faff; + border-radius: 16px; + padding: 20px; + margin-bottom: 16px; + border: 1px solid #eef1f9; } .meeting-preview-speaker-head { @@ -495,307 +742,54 @@ } .meeting-preview-speaker-avatar { + width: 40px; + height: 40px; + border-radius: 12px; + background: var(--primary-gradient); + color: white; display: flex; align-items: center; justify-content: center; - width: 42px; - height: 42px; - border-radius: 12px; - background: var(--preview-accent-soft); - color: var(--preview-accent); font-weight: 700; - font-size: 16px; } .meeting-preview-speaker-name { font-weight: 700; font-size: 15px; - margin-bottom: 4px; } .meeting-preview-speaker-role { - color: var(--preview-muted); font-size: 12px; + color: var(--text-secondary); +} + +.meeting-preview-keypoint { + display: flex; + gap: 16px; + padding: 16px; + border-radius: 16px; + background: #fff; + border: 1px solid var(--border-color); + margin-bottom: 12px; +} + +.meeting-preview-keypoint-index { + font-size: 18px; + font-weight: 800; + color: #dee2e6; } .meeting-preview-todo { display: flex; align-items: flex-start; gap: 12px; - padding: 16px; + padding: 12px 0; } .meeting-preview-todo-dot { - flex-shrink: 0; - width: 10px; - height: 10px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--primary-blue); margin-top: 6px; - border-radius: 50%; - border: 2px solid var(--preview-accent); - box-shadow: 0 0 0 6px var(--preview-accent-soft); -} - -.meeting-preview-markdown { - font-size: 15px; - line-height: 1.8; - color: var(--preview-ink); -} - -.meeting-preview-markdown h1, -.meeting-preview-markdown h2, -.meeting-preview-markdown h3 { - margin-top: 1.5em; - margin-bottom: 0.5em; - font-weight: 700; - color: var(--preview-ink); -} - -.meeting-preview-markdown p { - margin-bottom: 1em; -} - -.meeting-preview-markdown ul, -.meeting-preview-markdown ol { - margin-bottom: 1em; - padding-left: 20px; -} - -.meeting-preview-markdown blockquote { - margin: 0 0 1em; - padding: 10px 16px; - border-left: 3px solid var(--preview-accent); - border-radius: 0 8px 8px 0; - background: var(--preview-accent-soft); -} - -.meeting-preview-transcript-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.meeting-preview-transcript-item { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 12px 16px; - border-radius: 16px; - border: 1px solid transparent; - transition: all 0.3s ease; - cursor: pointer; -} - -.meeting-preview-transcript-item:hover { - background: var(--app-bg-surface-soft); - border-color: var(--preview-line); -} - -.meeting-preview-transcript-item.is-active { - background: var(--app-bg-surface); - border-color: var(--preview-accent); - box-shadow: 0 4px 12px rgba(22, 119, 255, 0.08); -} - -.meeting-preview-transcript-avatar { - flex-shrink: 0; - width: 40px; - height: 40px; - border-radius: 50%; - background: linear-gradient(135deg, #1677ff, #36cfc9); - color: #fff; - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 16px; -} - -.meeting-preview-transcript-content { - flex: 1; - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; -} - -.meeting-preview-transcript-meta { - display: flex; - align-items: center; - gap: 8px; -} - -.meeting-preview-transcript-speaker { - font-weight: 700; - font-size: 14px; - color: var(--app-text-main); -} - -.meeting-preview-transcript-time { - font-size: 12px; - color: var(--app-text-secondary); -} - -.meeting-preview-transcript-text { - padding: 14px 18px; - border-radius: 4px 16px 16px 16px; - background: var(--app-bg-surface); - border: 1px solid var(--app-border-color); - font-size: 15px; - line-height: 1.6; - color: var(--app-text-main); - white-space: pre-wrap; -} - -.meeting-preview-transcript-item.is-active .meeting-preview-transcript-text { - border-color: var(--preview-accent-soft); - background: #f0f5ff; -} - -.meeting-preview-transcript-text mark { - background: var(--preview-accent-soft); - color: var(--preview-accent); - border-radius: 4px; - padding: 0 4px; -} - -.meeting-preview-page .transcript-player { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - width: min(calc(100% - 32px), 860px); - z-index: 100; - display: flex; - align-items: center; - gap: 16px; - padding: 14px 16px; - border-radius: 18px; - background: var(--app-bg-surface); - border: 1px solid var(--app-border-color); - box-shadow: 0 16px 40px rgba(0, 0, 0, 0.12); - backdrop-filter: blur(24px); -} -.player-main-btn, -.player-ghost-btn { - border: 0; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; -} -.player-main-btn { - width: 44px; - height: 44px; - border-radius: 50%; - background: linear-gradient(135deg, var(--app-primary-color), #36cfc9); - color: #fff; - font-size: 18px; - box-shadow: 0 4px 12px rgba(22, 119, 255, 0.2); -} -.player-ghost-btn { - gap: 6px; - padding: 0 12px; - height: 38px; - border-radius: 12px; - background: var(--app-bg-surface-soft); - color: var(--app-primary-color); - font-weight: 700; -} -.player-progress-shell { - flex: 1; - display: flex; - flex-direction: column; - gap: 8px; - min-width: 0; -} -.player-time-row { - display: flex; - align-items: center; - justify-content: space-between; - color: var(--app-text-secondary); - font-size: 12px; - font-weight: 700; -} -.player-range { - width: 100%; - appearance: none; - height: 6px; - border-radius: 999px; - background: linear-gradient(90deg, var(--app-primary-color), var(--app-primary-color)) 0/0% 100% no-repeat, - var(--app-bg-surface-soft); - outline: none; -} -.player-range::-webkit-slider-thumb { - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: #fff; - border: 3px solid var(--app-primary-color); - box-shadow: 0 4px 12px rgba(22, 119, 255, 0.24); - cursor: pointer; -} -.player-range::-moz-range-thumb { - width: 16px; - height: 16px; - border-radius: 50%; - background: #fff; - border: 3px solid var(--app-primary-color); - box-shadow: 0 4px 12px rgba(22, 119, 255, 0.24); - cursor: pointer; -} - -@keyframes preview-rise { - 0% { transform: translateY(30px); opacity: 0; } - 100% { transform: translateY(0); opacity: 1; } -} - -@media (max-width: 768px) { - .meeting-preview-page { - padding: 16px 12px 32px; - } - - .meeting-preview-hero { - padding: 20px 16px; - } - - .meeting-preview-title { - font-size: 28px; - } - - .meeting-preview-metrics { - grid-template-columns: 1fr; - } - - .meeting-preview-chapter, - .meeting-preview-keypoint { - flex-direction: column; - padding: 16px; - gap: 12px; - } - - .meeting-preview-chapter-time, - .meeting-preview-keypoint-index { - width: auto; - height: 36px; - padding: 0 12px; - font-size: 14px; - border-radius: 8px; - } - - .meeting-preview-transcript-item { - gap: 8px; - } - - .meeting-preview-audio-player { - flex-direction: column; - align-items: stretch; - border-radius: 16px; - padding: 16px; - } - - .meeting-preview-download-btn { - width: 100%; - border-radius: 8px; - } } diff --git a/frontend/src/pages/business/MeetingPreview.tsx b/frontend/src/pages/business/MeetingPreview.tsx index 822a80c..6b08363 100644 --- a/frontend/src/pages/business/MeetingPreview.tsx +++ b/frontend/src/pages/business/MeetingPreview.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { Alert, Button, Empty, Input, Result, Segmented, Skeleton, Tabs, Tag, message } from "antd"; +import { Alert, Button, Empty, Input, Result, Skeleton, Tabs, message } from "antd"; import { useParams, useSearchParams } from "react-router-dom"; import { AudioOutlined, @@ -7,7 +7,6 @@ import { CaretRightFilled, ClockCircleOutlined, CopyOutlined, - FastForwardOutlined, FileTextOutlined, LockOutlined, PauseOutlined, @@ -17,6 +16,7 @@ import { UserOutlined, DownOutlined, UpOutlined, + LinkOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; import ReactMarkdown from "react-markdown"; @@ -25,6 +25,7 @@ import { getPublicMeetingPreview, resolveAudioMimeType, resolveMeetingPlaybackAudioUrl, + type MeetingChapterVO, type MeetingTranscriptVO, type MeetingVO, } from "../../api/business/meeting"; @@ -32,87 +33,117 @@ import { buildMeetingAnalysis } from "./meetingAnalysis"; import "./MeetingPreview.css"; type AnalysisTab = "chapters" | "speakers" | "actions" | "todos"; -type PreviewPageTab = "summary" | "transcript"; +type PreviewPageTab = "summary" | "catalog" | "transcript"; const TEXT = { - statusTranscribing: "\u8f6c\u5199\u4e2d", - statusSummarizing: "\u603b\u7ed3\u4e2d", - statusCompleted: "\u5df2\u5b8c\u6210", - statusPending: "\u5f85\u5904\u7406", - hintTranscribing: "\u4f1a\u8bae\u5185\u5bb9\u4ecd\u5728\u6574\u7406\u4e2d\uff0c\u9884\u89c8\u4f1a\u6301\u7eed\u8865\u5168\u3002", - hintSummarizing: "AI \u6b63\u5728\u751f\u6210\u4f1a\u8bae\u603b\u7ed3\uff0c\u5df2\u5b8c\u6210\u5185\u5bb9\u4f1a\u4f18\u5148\u5c55\u793a\u3002", - hintCompleted: "\u4f1a\u8bae\u7eaa\u8981\u3001\u5206\u6790\u548c\u8f6c\u5f55\u5185\u5bb9\u5df2\u751f\u6210\u5b8c\u6210\u3002", - hintPending: "\u5f53\u524d\u4f1a\u8bae\u5c1a\u672a\u751f\u6210\u5b8c\u6574\u5185\u5bb9\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002", - missingMeetingId: "\u672a\u63d0\u4f9b\u4f1a\u8bae\u7f16\u53f7", - loadFailed: "\u4f1a\u8bae\u9884\u89c8\u52a0\u8f7d\u5931\u8d25", - noMeetingData: "\u672a\u627e\u5230\u4f1a\u8bae\u6570\u636e", - previewLabel: "\u4f1a\u8bae\u9884\u89c8", - untitledMeeting: "\u672a\u547d\u540d\u4f1a\u8bae", - meetingTime: "\u4f1a\u8bae\u65f6\u95f4", - hostCreator: "\u4e3b\u6301/\u521b\u5efa", - participantsCount: "\u53c2\u4f1a\u4eba\u6570", - tagsCount: "\u6807\u7b7e\u6570\u91cf", - notSet: "\u672a\u8bbe\u7f6e", - notFilled: "\u672a\u586b\u5199", - pageSummary: "\u603b\u7ed3\u4e0e\u5206\u6790", - pageTranscript: "\u8f6c\u5f55\u4e0e\u97f3\u9891", - copyLink: "\u590d\u5236\u94fe\u63a5", - shareNow: "\u7acb\u5373\u5206\u4eab", - shareCopied: "\u9884\u89c8\u94fe\u63a5\u5df2\u590d\u5236", - shareFallbackCopied: "\u5f53\u524d\u8bbe\u5907\u4e0d\u652f\u6301\u7cfb\u7edf\u5206\u4eab\uff0c\u5df2\u4e3a\u4f60\u590d\u5236\u94fe\u63a5", - shareFailed: "\u5206\u4eab\u5931\u8d25\uff0c\u8bf7\u5148\u590d\u5236\u94fe\u63a5", - accessCheck: "\u8bbf\u95ee\u6821\u9a8c", - passwordRequired: "\u8be5\u4f1a\u8bae\u9700\u8981\u8bbf\u95ee\u5bc6\u7801", - passwordHint: "\u8bf7\u8f93\u5165\u4f1a\u8bae\u7684 access_password \u540e\u7ee7\u7eed\u8bbf\u95ee\u9884\u89c8\u5185\u5bb9\u3002", - passwordPlaceholder: "\u8bf7\u8f93\u5165 access_password", - openPreview: "\u8fdb\u5165\u9884\u89c8", - invalidPassword: "\u8bbf\u95ee\u5bc6\u7801\u9519\u8bef", - basicInfo: "\u57fa\u672c\u4fe1\u606f", - meetingOverview: "\u4f1a\u8bae\u6982\u51b5", - creator: "\u521b\u5efa\u4eba", - host: "\u4e3b\u6301\u4eba", - createdAt: "\u521b\u5efa\u65f6\u95f4", - audioStatus: "\u97f3\u9891\u72b6\u6001", - participants: "\u53c2\u4f1a\u4eba\u5458", - tags: "\u4f1a\u8bae\u6807\u7b7e", - aiAnalysis: "AI \u5206\u6790", - analysis: "\u4f1a\u8bae\u5206\u6790", - previewExtra: "\u9884\u89c8\u9875\u4ec5\u8bfb\u5c55\u793a", - audioPlaybackWarning: "\u97f3\u9891\u4fdd\u5b58\u5931\u8d25\uff0c\u53ef\u80fd\u5f71\u54cd\u56de\u653e\u3002", - summaryOverview: "\u5168\u6587\u6982\u8981", - summaryEmpty: "\u6682\u65e0\u6982\u8981\u5185\u5bb9", - analysisChapters: "\u7ae0\u8282", - analysisSpeakers: "\u53d1\u8a00\u4eba", - analysisKeyPoints: "\u5173\u952e\u8981\u70b9", - analysisTodos: "\u5f85\u529e\u4e8b\u9879", - noChapterAnalysis: "\u6682\u65e0\u7ae0\u8282\u5206\u6790", - noSpeakerAnalysis: "\u6682\u65e0\u53d1\u8a00\u4eba\u5206\u6790", - noKeyPoints: "\u6682\u65e0\u5173\u952e\u8981\u70b9", - noTodos: "\u6682\u65e0\u5f85\u529e\u4e8b\u9879", - chapterFallback: "\u7ae0\u8282", - speakerFallback: "\u53d1\u8a00\u4eba", - speakerSummary: "\u53d1\u8a00\u6982\u8ff0", - keyPointFallback: "\u8981\u70b9", - noChapterSummary: "\u6682\u65e0\u7ae0\u8282\u63cf\u8ff0", - noSpeakerSummary: "\u6682\u65e0\u53d1\u8a00\u603b\u7ed3", - noKeyPointSummary: "\u6682\u65e0\u8981\u70b9\u8bf4\u660e", - summarySection: "\u4f1a\u8bae\u7eaa\u8981", - fullSummary: "\u5b8c\u6574\u7eaa\u8981", - noSummary: "\u6682\u65e0\u4f1a\u8bae\u7eaa\u8981", - transcriptSection: "\u4f1a\u8bae\u8f6c\u5f55", - transcriptTitle: "\u9010\u6bb5\u8f6c\u5f55", - noDuration: "\u6682\u65e0\u65f6\u957f", - audioUnavailable: "\u97f3\u9891\u6587\u4ef6\u4e0d\u53ef\u7528\uff0c\u4ec5\u5c55\u793a\u8f6c\u5f55\u5185\u5bb9\u3002", - noTranscript: "\u6682\u65e0\u8f6c\u5f55\u5185\u5bb9", - unknownSpeaker: "\u672a\u77e5\u53d1\u8a00\u4eba", + statusTranscribing: "转写中", + statusSummarizing: "总结中", + statusCompleted: "已完成", + statusPending: "待处理", + hintTranscribing: "会议内容仍在整理中,预览会持续补全。", + hintSummarizing: "AI 正在生成会议总结,已完成内容会优先展示。", + hintCompleted: "会议纪要、分析和转录内容已生成完成。", + hintPending: "当前会议尚未生成完整内容,请稍后重试。", + missingMeetingId: "未提供会议编号", + loadFailed: "会议预览加载失败", + noMeetingData: "未找到会议数据", + previewLabel: "会议预览", + untitledMeeting: "未命名会议", + meetingTime: "会议时间", + hostCreator: "主持/创建", + participantsCount: "参会人数", + tagsCount: "标签数量", + notSet: "未设置", + notFilled: "未填写", + pageSummary: "AI 纪要", + pageCatalog: "AI 目录", + pageTranscript: "转录原文", + copyLink: "复制链接", + shareNow: "立即分享", + shareCopied: "预览链接已复制", + shareFallbackCopied: "当前设备不支持系统分享,已为你复制链接", + shareFailed: "分享失败,请先复制链接", + accessCheck: "访问校验", + passwordRequired: "该会议需要访问密码", + passwordHint: "请输入会议的 access_password 后继续访问预览内容。", + passwordPlaceholder: "请输入 access_password", + openPreview: "进入预览", + invalidPassword: "访问密码错误", + basicInfo: "基本信息", + meetingOverview: "会议概况", + creator: "创建人", + host: "主持人", + createdAt: "创建时间", + audioStatus: "音频状态", + participants: "人", + tags: "会议标签", + aiAnalysis: "AI 目录", + analysis: "会议分析", + previewExtra: "预览页仅读展示", + audioPlaybackWarning: "音频保存失败,可能影响回放。", + summaryOverview: "全文概要", + summaryEmpty: "暂无概要内容", + analysisChapters: "章节", + analysisSpeakers: "发言人", + analysisKeyPoints: "关键要点", + analysisTodos: "待办事项", + noChapterAnalysis: "暂无章节分析", + noSpeakerAnalysis: "暂无发言人分析", + noKeyPoints: "暂无关键要点", + noTodos: "暂无待办事项", + chapterFallback: "章节", + speakerFallback: "发言人", + speakerSummary: "发言概述", + keyPointFallback: "要点", + noChapterSummary: "暂无章节描述", + noSpeakerSummary: "暂无发言总结", + noKeyPointSummary: "暂无要点说明", + summarySection: "会议纪要", + fullSummary: "完整纪要", + noSummary: "暂无会议纪要", + transcriptSection: "会议转录", + transcriptTitle: "逐段转录", + noDuration: "暂无时长", + audioUnavailable: "音频文件不可用,仅展示转录内容。", + noTranscript: "暂无转录内容", + unknownSpeaker: "未知发言人", disclaimer: "智能内容由用户会议内容 + AI 模型生成,我们不对内容准确性和完整性做任何保证,亦不代表我们的观点或态度", - shareText: "\u6211\u5411\u4f60\u5206\u4eab\u4e86\u4e00\u4e2a\u4f1a\u8bae\u9884\u89c8\u94fe\u63a5", - audioSaved: "\u5df2\u4fdd\u5b58", - audioSaveFailed: "\u4fdd\u5b58\u5931\u8d25", - audioUploaded: "\u5df2\u4e0a\u4f20", - audioNotSaved: "\u672a\u4fdd\u5b58", + shareText: "我向你分享了一个会议预览链接", + audioSaved: "已保存", + audioSaveFailed: "保存失败", + audioUploaded: "已上传", + audioNotSaved: "未保存", + linkToTranscript: "关联原文", + noCatalog: "暂无 AI 目录", }; +type ChapterTranscriptLink = { + key: string; + title: string; + timeLabel: string; + transcriptIds: number[]; + firstTranscriptId: number | null; + firstTranscriptStartTime: number | null; +}; + +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; +} + const STATUS_META: Record = { 1: { label: TEXT.statusTranscribing, className: "is-processing", hint: TEXT.hintTranscribing }, 2: { label: TEXT.statusSummarizing, className: "is-processing", hint: TEXT.hintSummarizing }, @@ -169,9 +200,10 @@ export default function MeetingPreview() { const transcriptItemRefs = useRef>({}); const [meeting, setMeeting] = useState(null); const [transcripts, setTranscripts] = useState([]); + const [meetingChapters, setMeetingChapters] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [analysisTab, setAnalysisTab] = useState("chapters"); + const [analysisTab, setAnalysisTab] = useState("speakers"); const [pageTab, setPageTab] = useState("summary"); const [activeTranscriptId, setActiveTranscriptId] = useState(null); const [passwordRequired, setPasswordRequired] = useState(false); @@ -183,6 +215,8 @@ export default function MeetingPreview() { 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 [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" ? window.matchMedia("(max-width: 767px)").matches : false, ); @@ -202,6 +236,7 @@ export default function MeetingPreview() { setError(""); setMeeting(null); setTranscripts([]); + setMeetingChapters([]); setPasswordRequired(false); setPasswordVerified(false); setAccessPassword(presetAccessPassword); @@ -228,6 +263,7 @@ export default function MeetingPreview() { } setMeeting(previewRes.data.data.meeting); setTranscripts(previewRes.data.data.transcripts || []); + setMeetingChapters(previewRes.data.data.chapters || []); setPasswordVerified(true); return; } catch (requestError: any) { @@ -248,6 +284,7 @@ export default function MeetingPreview() { setMeeting(previewRes.data.data.meeting); setTranscripts(previewRes.data.data.transcripts || []); + setMeetingChapters(previewRes.data.data.chapters || []); setPasswordVerified(true); } catch (requestError: any) { if (!mounted) { @@ -307,18 +344,6 @@ export default function MeetingPreview() { className: "is-warning", hint: TEXT.hintPending, }; - const audioStatusLabel = useMemo(() => { - if (meeting?.audioSaveStatus === "SUCCESS") { - return TEXT.audioSaved; - } - if (meeting?.audioSaveStatus === "FAILED") { - return TEXT.audioSaveFailed; - } - if (meeting?.audioUrl) { - return TEXT.audioUploaded; - } - return TEXT.audioNotSaved; - }, [meeting?.audioSaveStatus, meeting?.audioUrl]); const shareUrl = typeof window !== "undefined" ? window.location.href : ""; const participantCountValue = isMobile && transcriptSpeakers.length > 0 ? transcriptSpeakers.length : participants.length; @@ -331,6 +356,66 @@ export default function MeetingPreview() { return 0; }, [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(item)) + .filter((item) => Number.isFinite(item) && transcriptIdToIndex.has(item)) + : []; + + if (sourceTranscriptIds.length) { + matchedTranscripts = sourceTranscriptIds + .map((item) => 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 > startMs); + if (firstTranscriptIndex >= 0) { + const lastTranscriptIndex = + nextChapterStartMs === undefined + ? transcripts.length + : transcripts.findIndex((item) => item.startTime >= 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]); + useEffect(() => { if (!activeTranscriptId) { return; @@ -341,7 +426,8 @@ export default function MeetingPreview() { return; } - target.scrollIntoView({ behavior: "smooth", block: "nearest" }); + // 使用 center 模式确保当前说话段落始终位于视口中央,避免被底部的浮动控件遮挡 + target.scrollIntoView({ behavior: "smooth", block: "center" }); }, [activeTranscriptId]); const handleTranscriptSeek = (item: MeetingTranscriptVO) => { @@ -353,6 +439,25 @@ export default function MeetingPreview() { audioRef.current.play().catch(() => {}); }; + const handleLocateChapterTranscript = (index: number) => { + const link = catalogChapterLinks[index]; + if (link && link.firstTranscriptId) { + setPageTab("transcript"); + setLinkedTranscriptIds(link.transcriptIds); + setLinkedChapterKey(link.key); + setActiveTranscriptId(link.firstTranscriptId); + + // 自动跳转并播放音频 + if (audioRef.current && link.firstTranscriptStartTime !== null) { + audioRef.current.currentTime = Math.max(0, link.firstTranscriptStartTime / 1000); + audioRef.current.play().catch(() => { + // 部分浏览器(尤其是移动端)可能会拦截非直接交互触发的播放 + // 但由于这是由用户点击目录项触发的,通常会被允许 + }); + } + } + }; + const toggleAudioPlayback = () => { if (!audioRef.current) return; if (audioPlaying) { @@ -431,19 +536,6 @@ export default function MeetingPreview() { setAudioPlaying(false); }; - const renderMeetingTitle = (title?: string) => { - const safeTitle = title || TEXT.untitledMeeting; - return safeTitle.split(/(\d+)/).map((part, index) => - /\d+/.test(part) ? ( - - {part} - - ) : ( - {part} - ), - ); - }; - const handlePasswordSubmit = async () => { if (!id) { return; @@ -455,6 +547,7 @@ export default function MeetingPreview() { const previewRes = await getPublicMeetingPreview(Number(id), accessPassword.trim()); setMeeting(previewRes.data.data.meeting); setTranscripts(previewRes.data.data.transcripts || []); + setMeetingChapters(previewRes.data.data.chapters || []); setPasswordVerified(true); } catch (requestError: any) { setPasswordError(requestError?.response?.data?.msg || requestError?.msg || TEXT.invalidPassword); @@ -568,142 +661,23 @@ export default function MeetingPreview() { ); } - const summaryTab = ( + const summaryTabContent = (
-
-
-
- - {TEXT.aiAnalysis} -
-

{TEXT.analysis}

-
-
{TEXT.previewExtra}
-
- - {meeting.audioSaveStatus === "FAILED" ? ( - - ) : null} - - {keywords.length > 0 ? ( -
- {keywords.map((item) => ( - - {item} - - ))} -
- ) : null} - -
-
{TEXT.summaryOverview}
-

{analysis.overview || TEXT.summaryEmpty}

-
- -
- {[ - { label: TEXT.analysisChapters, value: "chapters" }, - { label: TEXT.analysisSpeakers, value: "speakers" }, - { label: TEXT.analysisKeyPoints, value: "actions" }, - { label: TEXT.analysisTodos, value: "todos" }, - ].map((tab) => ( -
setAnalysisTab(tab.value as AnalysisTab)} - > - {tab.label} -
- ))} -
- -
- {analysisTab === "chapters" ? ( - analysis.chapters.length > 0 ? ( - analysis.chapters.map((item, index) => ( -
-
{item.time || "--:--"}
-
- {item.title || `${TEXT.chapterFallback} ${index + 1}`} - {item.summary || TEXT.noChapterSummary} +
+
+
关键词
+
+ {keywords.length ? ( + keywords.map((item) => ( +
+ #{item}
-
- )) - ) : ( -
{TEXT.noChapterAnalysis}
- ) - ) : null} - - {analysisTab === "speakers" ? ( - analysis.speakerSummaries.length > 0 ? ( - analysis.speakerSummaries.map((item, index) => ( -
-
-
{(item.speaker || "S").slice(0, 1)}
-
-
{item.speaker || `${TEXT.speakerFallback} ${index + 1}`}
-
{TEXT.speakerSummary}
-
-
-
{item.summary || TEXT.noSpeakerSummary}
-
- )) - ) : ( -
{TEXT.noSpeakerAnalysis}
- ) - ) : null} - - {analysisTab === "actions" ? ( - analysis.keyPoints.length > 0 ? ( - analysis.keyPoints.map((item, index) => ( -
-
{String(index + 1).padStart(2, "0")}
-
- {item.title || `${TEXT.keyPointFallback} ${index + 1}`} - {item.summary || TEXT.noKeyPointSummary} - {(item.speaker || item.time) ? ( -
- {item.speaker ? {item.speaker} : null} - {item.time ? {item.time} : null} -
- ) : null} -
-
- )) - ) : ( -
{TEXT.noKeyPoints}
- ) - ) : null} - - {analysisTab === "todos" ? ( - analysis.todos.length > 0 ? ( - analysis.todos.map((item, index) => ( -
- - {item} -
- )) - ) : ( -
{TEXT.noTodos}
- ) - ) : null} -
-
- -
-
-
-
- - {TEXT.summarySection} + )) + ) : ( + 暂无关键词 + )}
-

{TEXT.fullSummary}

@@ -718,15 +692,68 @@ export default function MeetingPreview() {
); - const transcriptTab = ( + const catalogTabContent = (
-
- - {TEXT.transcriptSection} -
+ {/*
*/} + {/* */} + {/* {TEXT.aiAnalysis}*/} + {/*
*/} +

{TEXT.pageCatalog}

+
+
+ +
+ {catalogChapterLinks.length ? ( + catalogChapterLinks.map((chapter, index) => ( +
+
+
+
+
+
handleLocateChapterTranscript(index)} + > +
{chapter.timeLabel}
+
+
{chapter.title}
+ +
+
+
+ )) + ) : ( + + )} +
+
+
+ ); + + const transcriptTabContent = ( +
+
+
+
+ {/*
*/} + {/* */} + {/* {TEXT.transcriptSection}*/} + {/*
*/}

{TEXT.transcriptTitle}

@@ -748,16 +775,26 @@ export default function MeetingPreview() { {transcripts.length > 0 ? ( 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 ${activeTranscriptId === item.id ? "is-active" : ""}`} - onClick={() => handleTranscriptSeek(item)} + className={`meeting-preview-transcript-item ${isActive ? "is-active" : ""} ${isLinked ? "is-linked" : ""}`} + onClick={() => { + handleTranscriptSeek(item); + setLinkedTranscriptIds([]); // Clear linked highlight on manual seek + setLinkedChapterKey(null); + }} > -
+
{(speakerKey || "S").slice(0, 1)}
@@ -782,156 +819,180 @@ export default function MeetingPreview() {
); + const 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")}`; + }; + return ( -
-
-
-
-
- - {TEXT.previewLabel} +
+
+
+ {/* Header Title Section */} +
+
+
- {statusMeta.label} -
- -
-
-

{renderMeetingTitle(meeting.title)}

-

{statusMeta.hint}

-
-
- - -
-
- -
-
-
- {TEXT.meetingTime} - - {meeting.meetingTime ? dayjs(meeting.meetingTime).format("YYYY.MM.DD HH:mm") : TEXT.notSet} +
+

{meeting.title || TEXT.untitledMeeting}

+
+ + {statusMeta.label} -
-
- {TEXT.createdAt} - - {meeting.createdAt ? dayjs(meeting.createdAt).format("YYYY.MM.DD HH:mm") : TEXT.notSet} - -
-
- {TEXT.creator} - {meeting.creatorName || TEXT.notSet} -
-
- {TEXT.host} - {meeting.hostName || TEXT.notSet} -
-
- {TEXT.participantsCount} - {participantCountValue || TEXT.notFilled} -
-
- 会议时长 - {meetingDuration > 0 ? formatDurationRange(0, meetingDuration).split(' - ')[1] : TEXT.noDuration} + {/*ID: {meeting.id}*/}
- - {participants.length > 0 ? ( -
-
{TEXT.participants}
-
- {participants.map((item) => ( - - - {item} - - ))} -
-
- ) : null} - - {tags.length > 0 ? ( -
0 ? 12 : 20 }}> -
{TEXT.tags}
-
- {tags.map((item) => ( - - {item} - - ))} -
-
- ) : null}
-
-
+ + {/* Sharing Buttons Bar */} +
+ +
-
-
-
- setPageTab(key as PreviewPageTab)} - items={[ - { key: "summary", label: TEXT.pageSummary, children: summaryTab }, - { key: "transcript", label: TEXT.pageTranscript, children: transcriptTab }, - ]} - /> -
-
+
+ {/* Main Content Area */} +
+
+
+ setPageTab(key as PreviewPageTab)} + items={[ + { key: "summary", label: TEXT.pageSummary }, + { key: "catalog", label: TEXT.pageCatalog }, + { key: "transcript", label: TEXT.pageTranscript }, + ]} + /> +
-
- - {TEXT.disclaimer} +
+ {pageTab === "summary" ? summaryTabContent : null} + {pageTab === "catalog" ? catalogTabContent : null} + {pageTab === "transcript" ? transcriptTabContent : null} +
+
+
+
+ +
+
+ + {TEXT.disclaimer} +
+
+ {/* Floating Audio Player - Permanent mount, visibility controlled */} {playbackAudioUrl && ( - - )} + - {playbackAudioUrl && pageTab === 'transcript' ? ( - <> -
-
- -
-
- {formatPlayerTime(audioCurrentTime)} - {formatPlayerTime(audioDuration)} -
+
+
{formatPlayerTime(audioCurrentTime)}
+
{formatPlayerTime(audioDuration)}
-
- - ) : null} +
+ )}
); }