diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index 6f49c15..45296e9 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -350,70 +350,6 @@ function parseChapterTimeToMs(value?: string) { return totalSeconds * 1000; } -/** - * 给 Markdown 文本中的关键词添加虚拟超链接 - */ -const linkifySummary = (content: string, keywords: string[]) => { - if (!content || !keywords.length) return content; - - // 按长度降序排列关键词,防止短词匹配长词的一部分 - const sortedKeywords = [...keywords].sort((a, b) => b.length - a.length); - const keywordPattern = sortedKeywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); - - // 这种正则替换需要非常小心,不要破坏已有的 Markdown 结构(链接、代码块等) - // 这里的策略是:先按代码块/链接分割,只在纯文本部分进行替换 - const parts = content.split(/(```[\s\S]*?```|`[^`]*`|\[[^\]]*\]\([^)]*\))/g); - - return parts.map(part => { - // 如果是代码块或已有链接,不处理 - if (part.startsWith('```') || part.startsWith('`') || part.startsWith('[')) { - return part; - } - // 在普通文本中查找并替换关键词 - return part.replace(new RegExp(`(${keywordPattern})`, 'g'), '[$1](#/keyword/$1)'); - }).join(''); -}; - -const MarkdownSummary: React.FC<{ - content: string; - keywords: string[]; - onKeywordClick: (keyword: string) => void; -}> = ({ content, keywords, onKeywordClick }) => { - const processedContent = useMemo(() => linkifySummary(content, keywords), [content, keywords]); - - return ( - { - // 检查是否是关键词链接(支持 URL 编码后的格式) - const isKeywordLink = href?.startsWith('#/keyword/') || (href && decodeURIComponent(href).startsWith('#/keyword/')); - - if (isKeywordLink) { - const decodedHref = decodeURIComponent(href!); - const keyword = decodedHref.replace('#/keyword/', ''); - return ( - { - e.preventDefault(); - e.stopPropagation(); - onKeywordClick(keyword); - }} - > - {children} - - ); - } - return {children}; - } - }} - > - {processedContent} - - ); -}; - const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => void; @@ -1860,88 +1796,38 @@ const MeetingDetail: React.FC = () => {
- -
-
关键词
-
- {analysis.keywords.length > 9 ? ( - - ) : null} - {isOwner && analysis.keywords.length > 0 ? ( - - ) : null} -
-
-
- {keywordItems.length ? ( - keywordItems.map((tag) => { - const isSelected = selectedKeywords.includes(tag); - const isHighlighted = highlightKeyword === tag; - return ( -
{ - if (isOwner && analysis.keywords.length) { - handleKeywordToggle(tag, !isSelected); - } - handleKeywordClick(tag); - }} - style={isHighlighted ? { borderColor: '#5f51ff', backgroundColor: 'rgba(95, 81, 255, 0.1)' } : {}} - > - #{tag} - {isOwner && isSelected && } -
- ); - }) - ) : ( - 暂无关键词 - )} -
-
-
- + AI 智能总结
- {meeting.summaryContent ? ( - // - ) : null} - {meeting.summaryContent && isOwner ? ( - - ) : null} + {isEditingSummary ? <> 保存 : <> 编辑} + + )} + {isEditingSummary && ( + setIsEditingSummary(false)}> + 取消 + + )}
@@ -1954,38 +1840,70 @@ const MeetingDetail: React.FC = () => { />
) : meeting.summaryContent ? ( -
-
- -
-
- ) : false ? ( - <> -
-
会议概述
- {analysis.overview ? ( -
-
220 ? 'summary-copy summary-fade' : 'summary-copy'}> - {analysis.overview} -
- {analysis.overview.length > 220 && ( - +
+ {/* Keywords placed between title and overview */} +
+
+
关键词
+ {isOwner && analysis.keywords.length > 0 && ( + )}
- ) : ( - 暂无概述 - )} -
+
+ {keywordItems.length ? ( + keywordItems.map((tag) => { + const isSelected = selectedKeywords.includes(tag); + return ( +
{ + if (isOwner && analysis.keywords.length) { + handleKeywordToggle(tag, !isSelected); + } + }} + > + #{tag} + {isOwner && isSelected && } +
+ ); + }) + ) : ( + 暂无关键词 + )} +
+
-
-
主要讨论点
- {discussionItems.length ? ( +
+
会议概述
+
+ {isEditingSummary ? ( + setSummaryDraft(e.target.value)} + autoSize={{ minRows: 10 }} + className="summary-inline-edit" + /> + ) : ( + + {analysis.overview || meeting.summaryContent} + + )} +
+
+ + {discussionItems.length > 0 && !isEditingSummary && ( +
+
主要讨论点
{discussionItems.map((item, index) => (
@@ -1993,37 +1911,15 @@ const MeetingDetail: React.FC = () => {
{item.title || `讨论点 ${index + 1}`} - {(item.speaker || item.time) && ( -
- {item.speaker ? {item.speaker} : null} - {item.time ? {item.time} : null} -
- )}
-
{item.summary || '暂无讨论摘要'}
+
{item.summary}
))}
- ) : ( - 暂无主要讨论点 - )} -
- - {analysis.todos.length ? ( -
-
待办事项
-
- {analysis.todos.map((item, index) => ( -
- - {item} -
- ))} -
- ) : null} - + )} +
) : (
@@ -2137,21 +2033,33 @@ const MeetingDetail: React.FC = () => { catalogChapterLinks.map((chapter, index) => (
-
{chapter.timeLabel}
-
-
{chapter.title}
- +
+
+
-
- )) +
handleLocateChapterTranscript(index)} + style={{ cursor: 'pointer' }} + > +
{chapter.timeLabel}
+
+
{chapter.title}
+ +
+
+
)) ) : ( )} @@ -2295,18 +2203,28 @@ const MeetingDetail: React.FC = () => { overflow: hidden; display: flex; flex-direction: column; - background: - radial-gradient(circle at top left, rgba(108, 103, 255, 0.08), transparent 22%), - linear-gradient(180deg, #f8faff 0%, #f3f6fc 100%); + background: + radial-gradient(circle at 20% 20%, rgba(95, 81, 255, 0.05) 0%, transparent 40%), + radial-gradient(circle at 80% 80%, rgba(108, 140, 255, 0.05) 0%, transparent 40%), + #fbfcfd; + position: relative; + } + .meeting-detail-page::before { + content: ""; + position: absolute; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3%3Ffilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); + opacity: 0.015; + pointer-events: none; } .meeting-detail-page-header { margin-bottom: 18px; - padding: 18px 20px; - border-radius: 24px; - border: 1px solid rgba(220, 226, 242, 0.9); - background: rgba(255, 255, 255, 0.92); - box-shadow: 0 18px 46px rgba(95, 109, 155, 0.1); - backdrop-filter: blur(18px); + padding: 24px 32px; + border-radius: 28px; + border: 1px solid rgba(220, 226, 242, 0.5); + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.03); + backdrop-filter: blur(20px); } .meeting-detail-title-wrap { display: flex; @@ -2337,10 +2255,11 @@ const MeetingDetail: React.FC = () => { flex-wrap: wrap; } .meeting-detail-title-text { - color: #16203d; - font-size: 18px; + color: #1a1f36; + font-size: 24px; line-height: 1.2; font-weight: 800; + letter-spacing: -0.03em; } .meeting-detail-title-edit { width: 28px; @@ -2401,37 +2320,72 @@ const MeetingDetail: React.FC = () => { .summary-panel .ant-card-body { display: flex; flex-direction: column; - gap: 24px; - padding: 22px 24px 24px; + gap: 20px; + padding: 24px; } .summary-head { display: flex; align-items: center; justify-content: space-between; - gap: 12px; + margin-bottom: 4px; } .summary-title { display: inline-flex; align-items: center; - gap: 10px; - color: #5f51ff; - font-size: 26px; + gap: 8px; + color: #1a1f36; + font-size: 18px; font-weight: 800; } .summary-head-actions { - display: inline-flex; - align-items: center; - gap: 4px; - } - .summary-progress-shell, - .summary-empty-state { - min-height: 280px; display: flex; align-items: center; - justify-content: center; + gap: 16px; } - .summary-markdown-panel { - min-height: 0; + .summary-head-link { + color: #6e7695; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + transition: color 0.2s; + } + .summary-head-link:hover { + color: #5f51ff; + } + + .summary-content-box { + background: #f8faff; + border: 1px solid #eef1f9; + border-radius: 16px; + padding: 24px; + } + .summary-section-title { + color: #9aa0bd; + font-size: 14px; + font-weight: 700; + margin-bottom: 12px; + letter-spacing: 0.02em; + } + .summary-markdown { + font-size: 15px; + line-height: 1.8; + color: #2d3553; + } + .summary-markdown p { + margin-bottom: 16px; + } + .summary-markdown ul { + padding-left: 20px; + margin-bottom: 16px; + } + .summary-markdown li { + margin-bottom: 8px; + position: relative; + } + .summary-markdown li::marker { + color: #5f51ff; } .keyword-panel .ant-card-body { display: grid; @@ -2807,24 +2761,29 @@ const MeetingDetail: React.FC = () => { .transcript-stage-tabs { display: flex; align-items: center; - gap: 20px; - padding: 0 18px; - min-height: 54px; - border-bottom: 1px solid rgba(228, 232, 245, 0.92); - background: rgba(255, 255, 255, 0.84); + gap: 32px; + padding: 0 24px; + min-height: 56px; + border-bottom: 1px solid #eef1f9; + background: #ffffff; } .transcript-stage-tabs button { padding: 0; border: 0; background: transparent; - color: #7d86a5; - font-size: 14px; + color: #6e7695; + font-size: 16px; font-weight: 700; height: 100%; + cursor: pointer; position: relative; + transition: color 0.2s; + } + .transcript-stage-tabs button:hover { + color: #5f51ff; } .transcript-stage-tabs button.active { - color: #4f56ff; + color: #5f51ff; } .transcript-stage-tabs button.active::after { content: ""; @@ -2832,9 +2791,9 @@ const MeetingDetail: React.FC = () => { left: 0; right: 0; bottom: 0; - height: 2px; - border-radius: 999px; + height: 3px; background: #5f51ff; + border-radius: 99px 99px 0 0; } .transcript-scroll-shell { flex: 1; @@ -2844,62 +2803,136 @@ const MeetingDetail: React.FC = () => { padding: 18px 18px 0; } .catalog-list { - display: grid; - gap: 14px; - padding-bottom: 0; + display: flex; + flex-direction: column; + gap: 0; + padding: 10px 0 20px; } - .catalog-item { - display: grid; - grid-template-columns: 76px minmax(0, 1fr); - gap: 14px; - align-items: start; - padding: 16px 18px; - border-radius: 18px; - border: 1px solid rgba(228, 232, 245, 0.96); - background: rgba(248, 250, 255, 0.96); - transition: all 0.24s ease; + .catalog-item-container { + display: flex; + gap: 20px; + padding: 0 10px; } - .catalog-item:hover { - border-color: rgba(95, 81, 255, 0.18); - box-shadow: 0 12px 28px rgba(95, 81, 255, 0.08); - transform: translateY(-1px); + .catalog-timeline-axis { + display: flex; + flex-direction: column; + align-items: center; + width: 20px; + flex-shrink: 0; + position: relative; } - .catalog-item.active { - border-color: rgba(95, 81, 255, 0.24); - background: linear-gradient(135deg, rgba(95, 81, 255, 0.08), rgba(108, 140, 255, 0.06)); - box-shadow: 0 14px 30px rgba(95, 81, 255, 0.1); + .catalog-timeline-dot { + width: 10px; + height: 10px; + background: #5f51ff; + border-radius: 50%; + margin-top: 24px; + z-index: 2; + transition: all 0.3s ease; } - .catalog-item-time { - color: #5c66a2; - font-size: 13px; - font-weight: 800; - letter-spacing: 0.04em; - padding-top: 4px; + .catalog-timeline-line { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + background: #eef1f9; + z-index: 1; } - .catalog-item-main { - min-width: 0; - display: grid; - gap: 10px; + .catalog-item-container:first-child .catalog-timeline-line { + top: 24px; } - .catalog-item-title { - color: #273153; - font-size: 15px; - font-weight: 700; - line-height: 1.7; + .catalog-item-container:last-child .catalog-timeline-line { + bottom: calc(100% - 34px); + } + .catalog-item-card { + flex: 1; + background: #ffffff; + border: 1px solid #eef1f9; + border-radius: 12px; + padding: 16px 20px; + margin-bottom: 20px; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + } + .catalog-item-card:hover { + background: #f9faff; + border-color: rgba(95, 81, 255, 0.2); } .catalog-item-link { - width: fit-content; - padding: 0; - border: 0; - background: transparent; + opacity: 0; + visibility: hidden; + padding: 4px 10px; + border-radius: 6px; + border: 1px solid transparent; + background: rgba(95, 81, 255, 0.06); color: #5f51ff; - font-size: 13px; - font-weight: 700; + font-size: 12px; + font-weight: 600; cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + transition: all 0.2s ease; + } + .catalog-item-container:hover .catalog-item-link { + opacity: 1; + visibility: visible; + } + + .summary-head-link--static { + cursor: default !important; + color: #9aa0bd !important; + } + .summary-head-link--static:hover { + color: #9aa0bd !important; + } + + .summary-inline-edit { + width: 100% !important; + border-radius: 12px !important; + border: 1px solid #d9d9d9 !important; + padding: 12px 16px !important; + font-size: 15px !important; + line-height: 1.8 !important; + color: #2d3553 !important; + background: #fff !important; + } + .summary-inline-edit:focus { + border-color: #5f51ff !important; + box-shadow: 0 0 0 2px rgba(95, 81, 255, 0.1) !important; } .catalog-item-link:hover { - color: #4536f0; - text-decoration: underline; + background: rgba(95, 81, 255, 0.12); + border-color: rgba(95, 81, 255, 0.2); + } + + .tag, + .summary-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + border-radius: 99px; + background: #ffffff; + color: #6e7695; + border: 1px solid #eef1f9; + font-size: 13px; + font-weight: 500; + transition: all 0.2s ease; + } + .selectable-tag:hover { + border-color: #5f51ff; + color: #5f51ff; + background: rgba(95, 81, 255, 0.02); + } + .selectable-tag.selected { + border-color: #5f51ff; + background: rgba(95, 81, 255, 0.06); + color: #5f51ff; + } + .selectable-tag.highlighted-tag { + border-color: #5f51ff; + background: rgba(95, 81, 255, 0.05); + color: #5f51ff; } .segmented-tabs { display: flex; @@ -3155,19 +3188,21 @@ const MeetingDetail: React.FC = () => { width: 100%; max-width: 100%; box-sizing: border-box; - padding: 14px 18px; - border-radius: 22px; - background: rgba(255, 255, 255, 0.96); - border: 1px solid rgba(228, 232, 245, 0.96); - color: #313b5b; - line-height: 1.74; + padding: 16px 22px; + border-radius: 20px; + background: #ffffff; + border: 1px solid rgba(228, 232, 245, 0.6); + color: #2d3553; + line-height: 1.8; font-size: 15px; white-space: pre-wrap; - transition: all 0.3s ease; + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02); } .transcript-bubble:hover { - border-color: rgba(95, 81, 255, 0.24); - box-shadow: 0 8px 24px rgba(95, 81, 255, 0.08); + border-color: rgba(95, 81, 255, 0.2); + box-shadow: 0 12px 24px rgba(95, 81, 255, 0.05); + transform: translateY(-1px); } .transcript-bubble.editable { cursor: text; @@ -3396,9 +3431,6 @@ const MeetingDetail: React.FC = () => { .detail-left-column { padding-right: 0; } - .catalog-item { - grid-template-columns: 68px minmax(0, 1fr); - } .speaker-summary-card { grid-template-columns: 1fr; } @@ -3420,10 +3452,6 @@ const MeetingDetail: React.FC = () => { .detail-left-column { overflow: visible; } - .catalog-item { - grid-template-columns: 1fr; - gap: 8px; - } .transcript-player--floating { bottom: 72px; max-width: calc(100vw - 24px); @@ -3494,11 +3522,9 @@ const MeetingDetail: React.FC = () => { /> ) : (
- + + {meeting.summaryContent} +
) ) : (