From db310fc8033b2f57b305318c639ee94cdce5d4e5 Mon Sep 17 00:00:00 2001 From: chenhao Date: Wed, 15 Apr 2026 17:55:57 +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=E5=8A=9F=E8=83=BD=E5=92=8C=E7=9B=B8=E5=85=B3?= =?UTF-8?q?UI=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `MeetingAccessService` 中添加 `isPreviewPasswordRequired` 和 `assertCanPreviewMeeting` 方法 - 在 `PromptTemplateVO` 和 `PromptTemplateDTO` 中添加 `description` 字段 - 在前端添加 `meetingAnalysis.ts` 以处理会议分析数据 - 在 `PromptTemplates.tsx` 中显示模板描述 - 添加 `MeetingPreview.tsx` 组件,支持会议预览和密码验证 --- backend/design/db_schema_pgsql.sql | 1 + .../legacy/LegacyPromptItemResponse.java | 2 + .../imeeting/dto/biz/PromptTemplateDTO.java | 1 + .../imeeting/dto/biz/PromptTemplateVO.java | 1 + .../imeeting/entity/biz/PromptTemplate.java | 2 + .../service/biz/MeetingAccessService.java | 4 + .../biz/impl/MeetingAccessServiceImpl.java | 28 + .../biz/impl/PromptTemplateServiceImpl.java | 2 + backend/src/main/resources/application.yml | 1 + frontend/src/api/business/meeting.ts | 24 + frontend/src/api/business/prompt.ts | 2 + .../src/pages/business/MeetingPreview.css | 591 +++++++++++++++++ .../src/pages/business/MeetingPreview.tsx | 595 ++++++++++++++++++ .../src/pages/business/PromptTemplates.tsx | 18 + .../src/pages/business/meetingAnalysis.ts | 190 ++++++ frontend/src/routes/index.tsx | 6 +- 16 files changed, 1463 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/business/MeetingPreview.css create mode 100644 frontend/src/pages/business/MeetingPreview.tsx create mode 100644 frontend/src/pages/business/meetingAnalysis.ts diff --git a/backend/design/db_schema_pgsql.sql b/backend/design/db_schema_pgsql.sql index b3f6877..5b5f1d6 100644 --- a/backend/design/db_schema_pgsql.sql +++ b/backend/design/db_schema_pgsql.sql @@ -294,6 +294,7 @@ CREATE TABLE biz_prompt_templates ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID (0为系统级) template_name VARCHAR(100) NOT NULL, -- 模板名称 + description VARCHAR(255), -- 模板描述 category VARCHAR(20), -- 分类 (字典: biz_prompt_category) is_system SMALLINT DEFAULT 0, -- 是否系统预置 (1:是, 0:否) creator_id BIGINT, -- 创建人ID diff --git a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyPromptItemResponse.java b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyPromptItemResponse.java index 0fc0782..8a80e04 100644 --- a/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyPromptItemResponse.java +++ b/backend/src/main/java/com/imeeting/dto/android/legacy/LegacyPromptItemResponse.java @@ -8,6 +8,7 @@ import lombok.Data; public class LegacyPromptItemResponse { private Long id; private String name; + private String description; @JsonProperty("is_default") private Integer isDefault; @@ -16,6 +17,7 @@ public class LegacyPromptItemResponse { LegacyPromptItemResponse response = new LegacyPromptItemResponse(); response.setId(source.getId()); response.setName(source.getTemplateName()); + response.setDescription(source.getDescription()); response.setIsDefault(defaultItem ? 1 : 0); return response; } diff --git a/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateDTO.java b/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateDTO.java index b1cb798..b52bb75 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateDTO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateDTO.java @@ -7,6 +7,7 @@ public class PromptTemplateDTO { private Long id; private Long tenantId; private String templateName; + private String description; private String category; private Integer isSystem; private java.util.List tags; diff --git a/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java b/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java index c18334a..f58d4d0 100644 --- a/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java +++ b/backend/src/main/java/com/imeeting/dto/biz/PromptTemplateVO.java @@ -9,6 +9,7 @@ public class PromptTemplateVO { private Long tenantId; private Long creatorId; private String templateName; + private String description; private String category; private Integer isSystem; private java.util.List tags; diff --git a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java index 8beaa33..cfcc5f2 100644 --- a/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java +++ b/backend/src/main/java/com/imeeting/entity/biz/PromptTemplate.java @@ -16,6 +16,8 @@ public class PromptTemplate extends BaseEntity { private String templateName; + private String description; + private String category; private Integer isSystem; diff --git a/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java b/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java index 021e03f..2d0077f 100644 --- a/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java +++ b/backend/src/main/java/com/imeeting/service/biz/MeetingAccessService.java @@ -6,6 +6,10 @@ import com.unisbase.security.LoginUser; public interface MeetingAccessService { Meeting requireMeeting(Long meetingId); + boolean isPreviewPasswordRequired(Meeting meeting); + + void assertCanPreviewMeeting(Meeting meeting, String accessPassword); + void assertCanViewMeeting(Meeting meeting, LoginUser loginUser); void assertCanEditMeeting(Meeting meeting, LoginUser loginUser); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java index d5ac508..1a89d17 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingAccessServiceImpl.java @@ -22,6 +22,26 @@ public class MeetingAccessServiceImpl implements MeetingAccessService { return meeting; } + @Override + public boolean isPreviewPasswordRequired(Meeting meeting) { + return normalizePreviewPassword(meeting == null ? null : meeting.getAccessPassword()) != null; + } + + @Override + public void assertCanPreviewMeeting(Meeting meeting, String accessPassword) { + String expectedPassword = normalizePreviewPassword(meeting == null ? null : meeting.getAccessPassword()); + if (expectedPassword == null) { + return; + } + String providedPassword = normalizePreviewPassword(accessPassword); + if (providedPassword == null) { + throw new RuntimeException("该会议需要访问密码"); + } + if (!expectedPassword.equals(providedPassword)) { + throw new RuntimeException("访问密码错误"); + } + } + @Override public void assertCanViewMeeting(Meeting meeting, LoginUser loginUser) { if (isPlatformAdmin(loginUser)) { @@ -104,4 +124,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService { String target = "," + loginUser.getUserId() + ","; return ("," + meeting.getParticipants() + ",").contains(target); } + + private String normalizePreviewPassword(String accessPassword) { + if (accessPassword == null) { + return null; + } + String normalized = accessPassword.trim(); + return normalized.isEmpty() ? null : normalized; + } } diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java index 94da8df..bd1a5a2 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/PromptTemplateServiceImpl.java @@ -163,6 +163,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl { return http.get<{ code: string; data: MeetingVO; msg: string }>( `/api/biz/meeting/${id}` @@ -222,6 +231,21 @@ export const getTranscripts = (id: number) => { ); }; +export const getMeetingPreviewAccess = (id: number) => { + return http.get<{ code: string; data: MeetingPreviewAccessVO; msg: string }>( + `/api/public/meetings/${id}/preview/access` + ); +}; + +export const getPublicMeetingPreview = (id: number, accessPassword?: string) => { + return http.get<{ code: string; data: PublicMeetingPreviewVO; msg: string }>( + `/api/public/meetings/${id}/preview`, + { + params: accessPassword ? { accessPassword } : undefined, + } + ); +}; + export interface MeetingSpeakerUpdateDTO { meetingId: number; speakerId: string; diff --git a/frontend/src/api/business/prompt.ts b/frontend/src/api/business/prompt.ts index c69a821..6468c90 100644 --- a/frontend/src/api/business/prompt.ts +++ b/frontend/src/api/business/prompt.ts @@ -5,6 +5,7 @@ export interface PromptTemplateVO { tenantId: number; creatorId: number; templateName: string; + description?: string; category: string; isSystem: number; tags?: string[]; @@ -19,6 +20,7 @@ export interface PromptTemplateVO { export interface PromptTemplateDTO { id?: number; templateName: string; + description?: string; category: string; isSystem: number; tags?: string[]; diff --git a/frontend/src/pages/business/MeetingPreview.css b/frontend/src/pages/business/MeetingPreview.css new file mode 100644 index 0000000..5aa7f19 --- /dev/null +++ b/frontend/src/pages/business/MeetingPreview.css @@ -0,0 +1,591 @@ +.meeting-preview-page { + --preview-bg: + radial-gradient(circle at top left, rgba(252, 208, 157, 0.24), transparent 32%), + radial-gradient(circle at top right, rgba(82, 164, 255, 0.18), transparent 28%), + linear-gradient(180deg, #fffaf4 0%, #f8f1e7 52%, #efe6da 100%); + --preview-ink: #33261c; + --preview-muted: rgba(72, 53, 39, 0.72); + --preview-line: rgba(84, 57, 31, 0.12); + --preview-card: rgba(255, 250, 244, 0.74); + --preview-card-strong: rgba(255, 252, 247, 0.88); + --preview-shadow: 0 24px 64px rgba(105, 72, 40, 0.12); + --preview-accent: #b86432; + --preview-accent-soft: rgba(184, 100, 50, 0.14); + --preview-cool: #315f8b; + position: relative; + min-height: 100vh; + padding: 24px 16px 40px; + background: var(--preview-bg); + color: var(--preview-ink); + overflow: hidden; +} + +.meeting-preview-page::before, +.meeting-preview-page::after { + content: ""; + position: fixed; + pointer-events: none; + z-index: 0; +} + +.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(184, 100, 50, 0.18), rgba(184, 100, 50, 0) 56%); + filter: blur(2px); +} + +.meeting-preview-page::after { + left: -48px; + bottom: 120px; + width: 160px; + height: 160px; + border-radius: 32px; + border: 1px solid rgba(49, 95, 139, 0.14); + background: + linear-gradient(135deg, rgba(49, 95, 139, 0.12), rgba(49, 95, 139, 0)), + rgba(255, 255, 255, 0.32); + transform: rotate(16deg); +} + +.meeting-preview-shell { + position: relative; + z-index: 1; + width: min(100%, 920px); + margin: 0 auto; +} + +.meeting-preview-loading, +.meeting-preview-empty { + display: grid; + gap: 18px; +} + +.meeting-preview-card { + position: relative; + overflow: hidden; + border: 1px solid var(--preview-line); + border-radius: 28px; + background: var(--preview-card); + box-shadow: var(--preview-shadow); + backdrop-filter: blur(18px); +} + +.meeting-preview-card::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(120deg, rgba(255, 255, 255, 0.66), rgba(255, 255, 255, 0.1) 42%, transparent 70%); + opacity: 0.8; + pointer-events: none; +} + +.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 { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.meeting-preview-eyebrow-label { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.62); + border: 1px solid rgba(84, 57, 31, 0.08); + color: var(--preview-muted); + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.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(59, 130, 89, 0.12); + color: #1f6a43; +} + +.meeting-preview-status.is-processing { + background: rgba(49, 95, 139, 0.12); + color: var(--preview-cool); +} + +.meeting-preview-status.is-warning { + background: rgba(184, 100, 50, 0.12); + color: var(--preview-accent); +} + +.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-subtitle { + margin: 14px 0 0; + color: var(--preview-muted); + font-size: 15px; + line-height: 1.75; +} + +.meeting-preview-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 22px; +} + +.meeting-preview-metric { + padding: 14px 14px 12px; + border-radius: 18px; + background: var(--preview-card-strong); + border: 1px solid rgba(84, 57, 31, 0.08); +} + +.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-section { + padding: 22px 18px; +} + +.meeting-preview-password-gate { + display: grid; + gap: 18px; +} + +.meeting-preview-password-form { + display: grid; + gap: 12px; +} + +.meeting-preview-section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.meeting-preview-section-kicker { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--preview-muted); + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.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-section-extra { + font-size: 12px; + color: var(--preview-muted); +} + +.meeting-preview-alert { + margin-bottom: 16px; + border-radius: 18px !important; +} + +.meeting-preview-tags { + 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 rgba(84, 57, 31, 0.08); + color: var(--preview-ink); + font-size: 13px; +} + +.meeting-preview-overview { + margin-top: 16px; + padding: 18px; + border-radius: 22px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0.4)), + rgba(255, 255, 255, 0.32); + border: 1px solid rgba(84, 57, 31, 0.08); +} + +.meeting-preview-overview-label { + margin-bottom: 8px; + color: var(--preview-muted); + font-size: 12px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.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 { + margin: 18px 0 14px; +} + +.meeting-preview-analysis-tabs .ant-segmented { + width: 100%; + padding: 5px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.5); +} + +.meeting-preview-analysis-tabs .ant-segmented-item { + min-height: 40px; + border-radius: 12px; +} + +.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 rgba(84, 57, 31, 0.08); + background: var(--preview-card-strong); + border-radius: 22px; +} + +.meeting-preview-chapter, +.meeting-preview-keypoint { + display: grid; + grid-template-columns: 70px minmax(0, 1fr); + gap: 12px; + padding: 14px; +} + +.meeting-preview-chapter-time, +.meeting-preview-keypoint-index { + display: flex; + align-items: flex-start; + justify-content: center; + padding: 10px 8px; + border-radius: 16px; + background: var(--preview-accent-soft); + color: var(--preview-accent); + font-weight: 700; + font-size: 13px; +} + +.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; +} + +.meeting-preview-meta-pill { + display: inline-flex; + align-items: center; + padding: 6px 10px; + border-radius: 999px; + background: rgba(49, 95, 139, 0.1); + color: var(--preview-cool); + font-size: 12px; +} + +.meeting-preview-speaker-card { + padding: 16px; +} + +.meeting-preview-speaker-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.meeting-preview-speaker-avatar { + display: grid; + place-items: center; + width: 42px; + height: 42px; + border-radius: 50%; + background: rgba(49, 95, 139, 0.14); + color: var(--preview-cool); + font-weight: 700; +} + +.meeting-preview-speaker-name { + font-size: 15px; + font-weight: 700; +} + +.meeting-preview-speaker-role { + margin-top: 2px; + color: var(--preview-muted); + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.meeting-preview-todo { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 14px 16px; +} + +.meeting-preview-todo-dot { + flex: none; + width: 10px; + height: 10px; + margin-top: 8px; + border-radius: 50%; + background: var(--preview-accent); + box-shadow: 0 0 0 6px rgba(184, 100, 50, 0.12); +} + +.meeting-preview-markdown { + padding: 4px 0 0; +} + +.meeting-preview-markdown .ant-empty { + padding: 24px 0 8px; +} + +.meeting-preview-markdown h1, +.meeting-preview-markdown h2, +.meeting-preview-markdown h3, +.meeting-preview-markdown h4 { + margin-top: 1.4em; + margin-bottom: 0.7em; + font-family: Georgia, "Times New Roman", "Songti SC", serif; + line-height: 1.2; + letter-spacing: -0.03em; +} + +.meeting-preview-markdown p, +.meeting-preview-markdown li { + color: var(--preview-ink); + font-size: 15px; + line-height: 1.92; +} + +.meeting-preview-markdown blockquote { + margin: 18px 0; + padding: 12px 16px; + border-left: 3px solid rgba(184, 100, 50, 0.4); + border-radius: 0 16px 16px 0; + background: rgba(184, 100, 50, 0.06); +} + +.meeting-preview-markdown pre { + overflow: auto; + padding: 14px; + border-radius: 18px; + background: rgba(51, 38, 28, 0.92); +} + +.meeting-preview-markdown code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; +} + +.meeting-preview-transcript-audio { + width: 100%; + margin-bottom: 16px; +} + +.meeting-preview-transcript-list { + display: grid; + gap: 12px; +} + +.meeting-preview-transcript-item { + width: 100%; + padding: 14px; + border: 1px solid rgba(84, 57, 31, 0.08); + border-radius: 22px; + background: var(--preview-card-strong); + text-align: left; + cursor: pointer; + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease, + background 0.2s ease; +} + +.meeting-preview-transcript-item:hover { + transform: translateY(-2px); + border-color: rgba(184, 100, 50, 0.22); + box-shadow: 0 12px 26px rgba(105, 72, 40, 0.08); +} + +.meeting-preview-transcript-item.is-active { + border-color: rgba(184, 100, 50, 0.28); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 244, 235, 0.92)); +} + +.meeting-preview-transcript-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.meeting-preview-transcript-speaker { + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.meeting-preview-transcript-avatar { + display: grid; + place-items: center; + width: 34px; + height: 34px; + border-radius: 50%; + font-size: 13px; + font-weight: 700; + color: #fff; +} + +.meeting-preview-transcript-name { + min-width: 0; + font-size: 14px; + font-weight: 700; +} + +.meeting-preview-transcript-time { + flex: none; + color: var(--preview-muted); + font-size: 12px; +} + +.meeting-preview-transcript-copy { + color: var(--preview-ink); + font-size: 14px; + line-height: 1.82; + white-space: pre-wrap; +} + +.meeting-preview-disclaimer { + margin-top: 18px; + padding: 0 8px; + color: rgba(72, 53, 39, 0.64); + font-size: 12px; + line-height: 1.8; + text-align: center; +} + +@keyframes preview-rise { + from { + opacity: 0; + transform: translateY(18px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (min-width: 768px) { + .meeting-preview-page { + padding: 36px 28px 56px; + } + + .meeting-preview-hero, + .meeting-preview-section { + padding-left: 24px; + padding-right: 24px; + } + + .meeting-preview-metrics { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .meeting-preview-panels { + gap: 22px; + } +} diff --git a/frontend/src/pages/business/MeetingPreview.tsx b/frontend/src/pages/business/MeetingPreview.tsx new file mode 100644 index 0000000..7def8fb --- /dev/null +++ b/frontend/src/pages/business/MeetingPreview.tsx @@ -0,0 +1,595 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useParams } from "react-router-dom"; +import { Alert, Button, Empty, Input, Result, Segmented, Skeleton, Tag } from "antd"; +import { + AudioOutlined, + CalendarOutlined, + ClockCircleOutlined, + FileTextOutlined, + LockOutlined, + RobotOutlined, + TeamOutlined, + UserOutlined, +} from "@ant-design/icons"; +import dayjs from "dayjs"; +import ReactMarkdown from "react-markdown"; +import { + getMeetingPreviewAccess, + getPublicMeetingPreview, + type MeetingTranscriptVO, + type MeetingVO, +} from "../../api/business/meeting"; +import { buildMeetingAnalysis } from "./meetingAnalysis"; +import "./MeetingPreview.css"; + +type AnalysisTab = "chapters" | "speakers" | "actions" | "todos"; + +const STATUS_META: Record = { + 1: { label: "閺夌儐鍓欓崯鎾寸▔?, className: "is-processing", hint: "濞村吋淇洪鍛村礃閸涱収鍟囧ù鐘茬Т濠€顏堝极鐎靛憡鍊炲☉鎿冨弿缁辨繃锛愰崟顕呮綌闁轰胶澧楀畵浣瑰濮橆厼鐦紓渚囧弨钘熼柛蹇嬪妸閳? }, + 2: { label: "闁诡剝宕电划銊︾▔?, className: "is-processing", hint: "AI 婵繐绲藉﹢顏堟偨閻旂鐏囬柟顒冨吹缁劑鏁嶅畝鍕垫殨閻熸瑥鐗撻妴澶愭椤厾绐楅柡鍕⒔閵囨艾顔忛幓鎺旀殮闁瑰瓨鍔楀▓鎴﹀礃閸涱収鍟囬柕? }, + 3: { label: "鐎瑰憡褰冮悾顒勫箣?, className: "is-complete", hint: "濞村吋淇洪鍛棯椤忓浂娲i柕鍡曠閸ㄥ酣寮搁幇顒佸闁告鍠愰弸鍐啅閼碱剚鏅搁柟瀛樺姇閻n剟骞嬮幇鈹惧亾? }, +}; + +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 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]; +} + +export default function MeetingPreview() { + const { id } = useParams(); + const audioRef = useRef(null); + const [meeting, setMeeting] = useState(null); + const [transcripts, setTranscripts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [analysisTab, setAnalysisTab] = useState("chapters"); + const [activeTranscriptId, setActiveTranscriptId] = useState(null); + const [passwordRequired, setPasswordRequired] = useState(false); + const [passwordVerified, setPasswordVerified] = useState(false); + const [accessPassword, setAccessPassword] = useState(""); + const [passwordError, setPasswordError] = useState(""); + + useEffect(() => { + let mounted = true; + + const fetchData = async () => { + if (!id) { + setError("闁哄牜浜濊ぐ浣圭瑹濞戞绐楅悹渚囧枤缁鳖亪宕?); + setLoading(false); + return; + } + + setLoading(true); + setError(""); + setMeeting(null); + setTranscripts([]); + setPasswordRequired(false); + setPasswordVerified(false); + setAccessPassword(""); + setPasswordError(""); + + try { + const meetingId = Number(id); + const accessRes = await getMeetingPreviewAccess(meetingId); + + if (!mounted) { + return; + } + + const requiresPassword = !!accessRes.data.data.passwordRequired; + setPasswordRequired(requiresPassword); + if (requiresPassword) { + setLoading(false); + return; + } + + const previewRes = await getPublicMeetingPreview(meetingId); + if (!mounted) { + return; + } + + setMeeting(previewRes.data.data.meeting); + setTranscripts(previewRes.data.data.transcripts || []); + setPasswordVerified(true); + } catch (requestError: any) { + if (!mounted) { + return; + } + + setError(requestError?.response?.data?.msg || "濞村吋淇洪鍛紣閸曨噮娼旈柛鏃傚Ь濞村洦寰勬潏顐バ?); + } finally { + if (mounted) { + setLoading(false); + } + } + }; + + fetchData(); + + return () => { + mounted = false; + }; + }, [id]); + + 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 statusMeta = STATUS_META[meeting?.status || 0] || { + label: "鐎垫澘鎳庨ˇ鈺呮偠?, + className: "is-warning", + hint: "鐟滅増鎸告晶鐘冲濮樻剚鍞村ù鐘茬У濠€顓㈡偨閻旂鐏囬悗鐟版湰閺嗭綁宕橀崨顓у晣闁挎稑鐭侀顒傜矙瀹ュ懏鍊甸柛鎰Х閻︻垶濡?, + }; + + const handleTranscriptSeek = (item: MeetingTranscriptVO) => { + if (!audioRef.current) { + return; + } + + audioRef.current.currentTime = Math.max(0, (item.startTime || 0) / 1000); + audioRef.current.play().catch(() => {}); + }; + + const handleAudioTimeUpdate = () => { + if (!audioRef.current || transcripts.length === 0) { + return; + } + + const currentMs = audioRef.current.currentTime * 1000; + const currentItem = transcripts.find( + (item) => currentMs >= (item.startTime || 0) && currentMs <= (item.endTime || 0), + ); + + setActiveTranscriptId(currentItem?.id || null); + }; + + const handlePasswordSubmit = async () => { + if (!id) { + return; + } + + setLoading(true); + setPasswordError(""); + try { + const previewRes = await getPublicMeetingPreview(Number(id), accessPassword.trim()); + setMeeting(previewRes.data.data.meeting); + setTranscripts(previewRes.data.data.transcripts || []); + setPasswordVerified(true); + } catch (requestError: any) { + setPasswordError(requestError?.response?.data?.msg || requestError?.msg || "閻犱礁娼″Λ鍓佲偓闈涙閻栨粓鏌ㄥ▎鎺濆殩"); + } finally { + setLoading(false); + } + }; + + if (loading && (!passwordRequired || passwordVerified)) { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+
+ ); + } + + if (passwordRequired && !passwordVerified) { + return ( +
+
+
+
+
+
+ + Access Check +
+

Password Required

+
+
+ +

+ Enter access_password to view this meeting preview. +

+ +
+ setAccessPassword(event.target.value)} + onPressEnter={handlePasswordSubmit} + /> + +
+ + {passwordError ? : null} +
+
+
+ ); + } + + if (error) { + return ( +
+
+
+ +
+
+ +
+
+
+
+ + 濞村吋淇洪鍛紣閸曨噮娼? +
+ {statusMeta.label} +
+ +

{meeting.title || "闁哄牜浜滈幊锟犲触瀹ュ嫮绐楅悹?}

+

{statusMeta.hint}

+ +
+
+ 濞村吋淇洪鍛村籍閸洘锛?/span> + + {meeting.meetingTime ? dayjs(meeting.meetingTime).format("YYYY.MM.DD HH:mm") : "闁哄牜浜i鏇犵磾?} + +
+
+ 濞戞挾绮€?闁告帗绋戠紓?/span> + {meeting.hostName || meeting.creatorName || "闁哄牜浜i鏇犵磾?} +
+
+ 闁告瑥鍊风槐鐗堢閻戞ɑ娈?/span> + {participants.length || "闁哄牜浜滈敐鐐哄礃?} +
+
+ 闁哄秴娲ㄩ鐑藉极娴兼潙娅?/span> + {tags.length || "闁哄牜浜i鏇犵磾?} +
+
+
+ +
+
+
+
+
+ + 闁糕晝鍎ゅ﹢鐗堢┍閳╁啩绱? +
+

濞村吋淇洪鍛潡閸屾艾鏋?/h2> +

+
+ +
+
+ 闁告帗绋戠紓鎾寸?/span> + {meeting.creatorName || "闁哄牜浜i鏇犵磾?} +
+
+ 濞戞挾绮€垫梹绂?/span> + {meeting.hostName || "闁哄牜浜i鏇犵磾?} +
+
+ 闁告帗绋戠紓鎾诲籍閸洘锛?/span> + + {meeting.createdAt ? dayjs(meeting.createdAt).format("YYYY.MM.DD HH:mm") : "闁哄牜浜i鏇犵磾?} + +
+
+ 闂傚﹥濞婇。鍫曟偐閼哥鍋?/span> + {meeting.audioSaveStatus || "NONE"} +
+
+ + {participants.length > 0 ? ( +
+
闁告瑥鍊风槐鐗堢閸濆嫭鍠?/div> +
+ {participants.map((item) => ( + + + {item} + + ))} +
+
+ ) : null} + + {tags.length > 0 ? ( +
+
濞村吋淇洪鍛村冀閸モ晩鍔?/div> +
+ {tags.map((item) => ( + + {item} + + ))} +
+
+ ) : null} +
+ +
+
+
+
+ + 闁哄懘缂氶崗姗€鏌呴悢娲绘綌 +
+

濞村吋淇洪鍛村礆閸℃鈧?/h2> +

+
濞戞挸绨肩槐鎵媼椤旀鍤婇柟顖氭噸缁绘岸骞愭担鍛婂€遍柛娆欑到缁?/div> +
+ + {meeting.status < 3 ? ( + + ) : null} + + {analysis.keywords.length > 0 ? ( +
+ {analysis.keywords.map((item) => ( + + {item} + + ))} +
+ ) : null} + +
+
闁稿繈鍔嶉弸鍐潡閸屾繍娲?/div> +

{analysis.overview || "闁哄棗鍊瑰Λ銈咁潡閸屾繍娲i柛鎰噹椤?}

+
+ +
+ + block + value={analysisTab} + onChange={(value) => setAnalysisTab(value)} + options={[ + { label: "缂佹梻濮炬俊?, value: "chapters" }, + { label: "闁告瑦鍨奸埢?, value: "speakers" }, + { label: "閻熸洑鑳堕崑?, value: "actions" }, + { label: "鐎垫澘鎳庢慨?, value: "todos" }, + ]} + /> +
+ +
+ {analysisTab === "chapters" && + (analysis.chapters.length ? ( + analysis.chapters.map((item, index) => ( +
+
{item.time || "--:--"}
+
+ {item.title || `缂佹梻濮炬俊?${index + 1}`} + {item.summary || "闁哄棗鍊瑰Λ銈囩博閻樺搫螡闁硅绻楅崼?} +
+
+ )) + ) : ( +
+ ( +
+
+
{(item.speaker || "闁?).slice(0, 1)}
+
+
{item.speaker || `闁告瑦鍨奸埢鍫熺?${index + 1}`}
+
闁告瑦鍨奸埢鍫濐潡閸屾繂鐗?/div> +
+
+
{item.summary || "闁哄棗鍊瑰Λ銈夊矗閹达絺鏋呴柟顒冨吹缁?}
+
+ )) + ) : ( +
+ ( +
+
{String(index + 1).padStart(2, "0")}
+
+ {item.title || `閻熸洑鑳堕崑?${index + 1}`} + {item.summary || "闁哄棗鍊瑰Λ銈囨啺娴e搫浠悹鍥х摠濡?} + {(item.speaker || item.time) && ( +
+ {item.speaker ? {item.speaker} : null} + {item.time ? {item.time} : null} +
+ )} +
+
+ )) + ) : ( +
+ ( +
+ + {item} +
+ )) + ) : ( +
+ +
+
+
+ + AI 闁诡剝宕电划? +
+

閻庣懓鏈弳锝囩棯椤忓浂娲?/h2> +

+
+ +
+ {meeting.summaryContent ? ( +
+ {meeting.summaryContent} +
+ ) : ( + +
+
+
+ + 闁告鍠愰弸鍐媼閺夎法绉? +
+

濞村吋淇洪鍛姜椤掆偓缂?/h2> +

+
+ + 闁绘劗鎳撻崵顔尖枔娴e啯鍎伴柛娆樺灥閻戯附娼鍕従濡? +
+
+ + {meeting.audioSaveStatus === "FAILED" ? ( + + ) : null} + + {meeting.audioUrl ? ( +
+
+ +
+ 闁哄懘缂氶崗姗€宕橀崨顓у晣闁?AI 婵☆垪鈧磭鈧兘鎮介悢绋跨亣闁挎稑濂旂划搴ㄦ偨閵娿倗鑹惧ù鍏间亢椤斿懏绌遍埄鍐х礀濡澘瀚~宥夋晬瀹€鍐惧殲缂備焦鎸搁幃搴ㄥ储閻斿娼楀ù鍏间亢椤斿懐鎷犻鐑嗘殧閻庣櫢绻濆Σ鍕椽瀹€鈧垾妯兼媼閵堝啠鍋? +
+
+
+ ); +} diff --git a/frontend/src/pages/business/PromptTemplates.tsx b/frontend/src/pages/business/PromptTemplates.tsx index ff37877..c6d3a0b 100644 --- a/frontend/src/pages/business/PromptTemplates.tsx +++ b/frontend/src/pages/business/PromptTemplates.tsx @@ -141,6 +141,11 @@ const PromptTemplates: React.FC = () => { icon: null, content: (
+ {record.description ? ( +
+ {record.description} +
+ ) : null} {record.promptContent}
), @@ -250,6 +255,11 @@ const PromptTemplates: React.FC = () => {
{item.templateName} + {item.description ? ( + + {item.description} + + ) : null} {/*使用次数: {item.usageCount || 0}*/}
@@ -399,6 +409,14 @@ const PromptTemplates: React.FC = () => { + + +