修复了前端的问题

main
mula.liu 2025-11-11 18:38:51 +08:00
parent 06f1b959b4
commit e5b04ed2d6
18 changed files with 539 additions and 284 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
dist.zip

Binary file not shown.

BIN
src/.DS_Store vendored 100644

Binary file not shown.

View File

@ -77,6 +77,7 @@ const ContentViewer = ({
<MindMap
content={content}
title={title}
initialScale={1.8}
/>
) : (
<div className="empty-content">等待内容生成后查看脑图</div>

View File

@ -0,0 +1,101 @@
/* Dropdown Container */
.dropdown-container {
position: relative;
display: inline-block;
}
.dropdown-trigger-wrapper {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Dropdown Menu */
.dropdown-menu-wrapper {
position: absolute;
top: calc(100% + 0.5rem);
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #e2e8f0;
padding: 0.5rem;
min-width: 140px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 2px;
animation: dropdown-fade-in 0.2s ease;
}
@keyframes dropdown-fade-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Alignment */
.dropdown-align-left {
left: 0;
}
.dropdown-align-right {
right: 0;
}
/* Menu Item */
.dropdown-menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1rem;
border: none;
background: none;
cursor: pointer;
border-radius: 6px;
font-size: 0.9375rem;
font-weight: 500;
color: #374151;
text-decoration: none;
transition: all 0.2s ease;
width: 100%;
text-align: left;
white-space: nowrap;
}
.dropdown-menu-item:hover:not(:disabled) {
background: #f3f4f6;
color: #111827;
}
.dropdown-menu-item:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Danger Item */
.dropdown-menu-item.danger {
color: #dc2626;
}
.dropdown-menu-item.danger:hover:not(:disabled) {
background: #fef2f2;
color: #b91c1c;
}
/* Item Icon and Label */
.dropdown-item-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.dropdown-item-label {
flex: 1;
}

View File

@ -0,0 +1,81 @@
import React, { useState, useEffect, useRef } from 'react';
import './Dropdown.css';
/**
* Dropdown - 通用下拉菜单组件
*
* @param {Object} props
* @param {React.ReactNode} props.trigger - 触发器元素按钮
* @param {Array<Object>} props.items - 菜单项数组
* - label: string - 显示文本
* - icon: React.ReactNode - 图标可选
* - onClick: function - 点击回调
* - className: string - 自定义样式类可选
* - danger: boolean - 是否为危险操作红色
* @param {string} props.align - 对齐方式: 'left' | 'right'默认 'right'
* @param {string} props.className - 外层容器自定义类名
*/
const Dropdown = ({
trigger,
items = [],
align = 'right',
className = ''
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
//
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isOpen]);
const handleTriggerClick = (e) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(!isOpen);
};
const handleItemClick = (item, e) => {
e.preventDefault();
e.stopPropagation();
if (item.onClick) {
item.onClick(e);
}
setIsOpen(false);
};
return (
<div className={`dropdown-container ${className}`} ref={dropdownRef}>
<div className="dropdown-trigger-wrapper" onClick={handleTriggerClick}>
{trigger}
</div>
{isOpen && (
<div className={`dropdown-menu-wrapper dropdown-align-${align}`}>
{items.map((item, index) => (
<button
key={index}
className={`dropdown-menu-item ${item.danger ? 'danger' : ''} ${item.className || ''}`}
onClick={(e) => handleItemClick(item, e)}
disabled={item.disabled}
>
{item.icon && <span className="dropdown-item-icon">{item.icon}</span>}
<span className="dropdown-item-label">{item.label}</span>
</button>
))}
</div>
)}
</div>
);
};
export default Dropdown;

View File

@ -78,7 +78,7 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 100;
min-width: 160px;
overflow: hidden;
overflow: visible;
}
.dropdown-menu button {

View File

@ -99,68 +99,6 @@
margin-bottom: 0.75rem;
}
.meeting-actions {
position: relative;
margin-left: 1rem;
flex-shrink: 0;
}
.dropdown-trigger {
background: transparent;
border: none;
padding: 0.25rem;
border-radius: 4px;
cursor: pointer;
color: #64748b;
transition: all 0.3s ease;
}
.dropdown-trigger:hover {
background: #f1f5f9;
color: #334155;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #e2e8f0;
min-width: 120px;
z-index: 10;
overflow: hidden;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: none;
border: none;
width: 100%;
text-align: left;
font-size: 0.9rem;
color: #334155;
text-decoration: none;
cursor: pointer;
transition: background 0.2s ease;
}
.dropdown-item:hover {
background: #f8fafc;
}
.dropdown-item.delete-item {
color: #ef4444;
}
.dropdown-item.delete-item:hover {
background: #fef2f2;
}
/* Delete Modal */
.delete-modal-overlay {
position: fixed;
@ -246,13 +184,6 @@
}
/* Dropdown Menu Styles */
.meeting-actions {
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
}
.dropdown-trigger {
background: none;
border: none;
@ -271,66 +202,6 @@
color: #334155;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #e2e8f0;
padding: 0.5rem;
margin-top: 0.5rem;
min-width: 120px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 2px;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: none;
background: none;
cursor: pointer;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
text-decoration: none;
transition: all 0.2s ease;
width: 100%;
text-align: left;
}
/* 确保Link组件内的dropdown-item正确显示 */
.dropdown-menu a {
text-decoration: none;
display: block;
width: 100%;
}
.dropdown-menu a .dropdown-item {
width: 100%;
}
.dropdown-item:hover {
background: #f3f4f6;
color: #111827;
}
.dropdown-item.delete-item {
color: #dc2626;
}
.dropdown-item.delete-item:hover {
background: #fef2f2;
color: #b91c1c;
}
.meeting-content {
background: white;
border-radius: 12px;

View File

@ -7,23 +7,12 @@ import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import TagDisplay from './TagDisplay';
import ConfirmDialog from './ConfirmDialog';
import Dropdown from './Dropdown';
import './MeetingTimeline.css';
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore = false, onLoadMore, loadingMore = false }) => {
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [showDropdown, setShowDropdown] = useState(null);
const navigate = useNavigate();
// Close dropdown when clicking outside
React.useEffect(() => {
const handleClickOutside = () => {
setShowDropdown(null);
};
if (showDropdown) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
}, [showDropdown]);
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '时间待定';
@ -67,19 +56,15 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore
return lines.length > maxLines || summary.length > maxLength;
};
const handleEditClick = (meetingId, e) => {
e.preventDefault();
navigate(`/meetings/edit/${meetingId}`)
setShowDropdown(null);
const handleEditClick = (meetingId) => {
navigate(`/meetings/edit/${meetingId}`);
};
const handleDeleteClick = (meeting, e) => {
e.preventDefault();
const handleDeleteClick = (meeting) => {
setDeleteConfirmInfo({
id: meeting.meeting_id,
title: meeting.title
});
setShowDropdown(null);
};
const handleConfirmDelete = async () => {
@ -89,12 +74,6 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore
setDeleteConfirmInfo(null);
};
const toggleDropdown = (meetingId, e) => {
e.preventDefault();
e.stopPropagation();
setShowDropdown(showDropdown === meetingId ? null : meetingId);
};
const sortedDates = Object.keys(meetingsByDate).sort((a, b) => new Date(b) - new Date(a));
if (sortedDates.length === 0) {
@ -142,31 +121,27 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore
</h3>
</div>
{isCreator && (
<div className="meeting-actions">
<button
className="dropdown-trigger"
onClick={(e) => toggleDropdown(meeting.meeting_id, e)}
>
<MoreVertical size={18} />
</button>
{showDropdown === meeting.meeting_id && (
<div className="dropdown-menu" onClick={(e) => e.stopPropagation()}>
<button className="dropdown-item"
onClick={(e) =>handleEditClick(meeting.meeting_id, e)}
>
<Edit size={16} />
编辑
</button>
<button
className="dropdown-item delete-item"
onClick={(e) => handleDeleteClick(meeting, e)}
>
<Trash2 size={16} />
删除
</button>
</div>
)}
</div>
<Dropdown
trigger={
<button className="dropdown-trigger">
<MoreVertical size={18} />
</button>
}
items={[
{
icon: <Edit size={16} />,
label: '编辑',
onClick: () => handleEditClick(meeting.meeting_id)
},
{
icon: <Trash2 size={16} />,
label: '删除',
onClick: () => handleDeleteClick(meeting),
danger: true
}
]}
align="right"
/>
)}
</div>
<div className="meeting-meta">

View File

@ -14,8 +14,9 @@ import { Loader } from 'lucide-react';
* @param {Object} props
* @param {string} props.content - Markdown格式的内容必须由父组件准备好
* @param {string} props.title - 标题用于显示
* @param {number} props.initialScale - 初始缩放倍数默认为1.8
*/
const MindMap = ({ content, title }) => {
const MindMap = ({ content, title, initialScale = 1.8 }) => {
const [markdown, setMarkdown] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@ -144,11 +145,6 @@ const MindMap = ({ content, title }) => {
try {
const processedMarkdown = preprocessMarkdownForMindMap(markdown, title);
// console.log('=== ===');
// console.log('markdown:');
// console.log(markdown);
// console.log('markdown:');
// console.log(processedMarkdown);
const transformer = new Transformer();
const { root } = transformer.transform(processedMarkdown);
@ -165,13 +161,9 @@ const MindMap = ({ content, title }) => {
setTimeout(() => {
if (markmapRef.current) {
markmapRef.current.fit();
// fit1.8
//
try {
const { state } = markmapRef.current;
if (state) {
const currentScale = state.transform.k;
markmapRef.current.rescale(currentScale * 1.8);
}
markmapRef.current.rescale(initialScale);
} catch (e) {
console.log('缩放调整失败:', e);
}
@ -183,7 +175,7 @@ const MindMap = ({ content, title }) => {
setError('思维导图渲染失败');
}
}, [markdown, loading, title]);
}, [markdown, loading, title, initialScale]);
if (loading) {
return (

View File

@ -0,0 +1,64 @@
/* 简洁搜索输入框样式 */
.simple-search-input {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
.simple-search-input-field {
width: 100%;
padding: 0.5rem 0.75rem;
padding-right: 2.5rem; /* 为清除按钮留出空间 */
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 0.875rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
outline: none;
}
.simple-search-input-field:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.simple-search-input-field::placeholder {
color: #94a3b8;
}
.simple-search-clear-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
padding: 0.25rem;
border: none;
background: transparent;
color: #94a3b8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
}
.simple-search-clear-btn:hover {
background: #f1f5f9;
color: #475569;
}
.simple-search-clear-btn:active {
background: #e2e8f0;
}
/* 禁用状态 */
.simple-search-input-field:disabled {
background: #f8fafc;
color: #94a3b8;
cursor: not-allowed;
}
.simple-search-input-field:disabled + .simple-search-clear-btn {
display: none;
}

View File

@ -0,0 +1,90 @@
import React, { useRef, useEffect, useState } from 'react';
import { X } from 'lucide-react';
import './SimpleSearchInput.css';
const SimpleSearchInput = ({
value = '',
onChange,
placeholder = '搜索...',
className = '',
debounceDelay = 500,
realTimeSearch = true
}) => {
const debounceTimerRef = useRef(null);
const [localValue, setLocalValue] = useState(value);
// value
useEffect(() => {
setLocalValue(value);
}, [value]);
const handleInputChange = (e) => {
const inputValue = e.target.value;
//
setLocalValue(inputValue);
//
if (onChange) {
if (realTimeSearch) {
// 使
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
onChange(inputValue);
}, debounceDelay);
} else {
// 使
onChange(inputValue);
}
}
};
const handleClear = () => {
//
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
//
setLocalValue('');
if (onChange) {
onChange('');
}
};
//
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, []);
return (
<div className={`simple-search-input ${className}`}>
<input
type="text"
placeholder={placeholder}
value={localValue}
onChange={handleInputChange}
className="simple-search-input-field"
/>
{localValue && (
<button
type="button"
className="simple-search-clear-btn"
onClick={handleClear}
aria-label="清除搜索"
>
<X size={16} />
</button>
)}
</div>
);
};
export default SimpleSearchInput;

View File

@ -449,7 +449,6 @@
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.section-header {
@ -583,43 +582,6 @@
background: #f1f5f9;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #e2e8f0;
padding: 0.5rem;
margin-top: 0.5rem;
width: 160px;
z-index: 10;
display: flex;
flex-direction: column;
}
.dropdown-menu button,
.dropdown-menu a {
background: none;
border: none;
text-align: left;
padding: 0.75rem 1rem;
width: 100%;
border-radius: 6px;
cursor: pointer;
color: #334155;
text-decoration: none;
display: flex;
align-items: center;
gap: 0.75rem;
}
.dropdown-menu button:hover,
.dropdown-menu a:hover {
background: #f1f5f9;
}
/* Modal Styles */
.modal-overlay {
position: fixed;

View File

@ -5,11 +5,12 @@ import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import MeetingTimeline from '../components/MeetingTimeline';
import TagCloud from '../components/TagCloud';
import ExpandSearchBox from '../components/ExpandSearchBox';
import SimpleSearchInput from '../components/SimpleSearchInput';
import VoiceprintCollectionModal from '../components/VoiceprintCollectionModal';
import ConfirmDialog from '../components/ConfirmDialog';
import PageLoading from '../components/PageLoading';
import ScrollToTop from '../components/ScrollToTop';
import Dropdown from '../components/Dropdown';
import meetingCacheService from '../services/meetingCacheService';
import './Dashboard.css';
@ -24,14 +25,12 @@ const Dashboard = ({ user, onLogout }) => {
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordChangeError, setPasswordChangeError] = useState('');
const [passwordChangeSuccess, setPasswordChangeSuccess] = useState('');
const dropdownRef = useRef(null);
//
const [voiceprintStatus, setVoiceprintStatus] = useState(null);
@ -206,18 +205,6 @@ const Dashboard = ({ user, onLogout }) => {
setSearchQuery('');
};
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setDropdownOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [dropdownRef]);
const fetchUserData = async () => {
try {
console.log('Fetching user data for user_id:', user.user_id);
@ -359,22 +346,38 @@ const Dashboard = ({ user, onLogout }) => {
<span className="logo-text">iMeeting</span>
</div>
<div className="user-actions">
<div className="user-menu-container" ref={dropdownRef}>
<div className="user-menu-trigger" onClick={() => setDropdownOpen(!dropdownOpen)}>
<span className="welcome-text">欢迎{userInfo?.caption}</span>
<ChevronDown size={20} />
</div>
{dropdownOpen && (
<div className="dropdown-menu">
<button onClick={() => { setShowChangePasswordModal(true); setDropdownOpen(false); }}><KeyRound size={16} /> 修改密码</button>
<Link to="/prompt-management" onClick={() => setDropdownOpen(false)}><BookText size={16} /> 提示词仓库</Link>
{user.role_id === 1 && (
<Link to="/admin/management" onClick={() => setDropdownOpen(false)}><Shield size={16} /> 平台管理</Link>
)}
<button onClick={onLogout}><LogOut size={16} /> 退出登录</button>
<Dropdown
trigger={
<div className="user-menu-trigger">
<span className="welcome-text">欢迎{userInfo?.caption}</span>
<ChevronDown size={20} />
</div>
)}
</div>
}
items={[
{
icon: <KeyRound size={16} />,
label: '修改密码',
onClick: () => setShowChangePasswordModal(true)
},
{
icon: <BookText size={16} />,
label: '提示词仓库',
onClick: () => window.location.href = '/prompt-management'
},
...(user.role_id === 1 ? [{
icon: <Shield size={16} />,
label: '平台管理',
onClick: () => window.location.href = '/admin/management'
}] : []),
{
icon: <LogOut size={16} />,
label: '退出登录',
onClick: onLogout
}
]}
align="right"
className="user-menu-dropdown"
/>
</div>
</div>
</header>
@ -484,13 +487,12 @@ const Dashboard = ({ user, onLogout }) => {
{/* 搜索和标签过滤卡片 */}
<div className="filter-card-wrapper">
<div className="filter-card-search">
<ExpandSearchBox
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
<SimpleSearchInput
value={searchQuery}
onChange={setSearchQuery}
placeholder="搜索会议名称或发起人..."
collapsedText="会议搜索"
realTimeSearch={true}
showIcon={true}
debounceDelay={500}
/>
</div>

View File

@ -7,6 +7,7 @@ import ContentViewer from '../components/ContentViewer';
import TagDisplay from '../components/TagDisplay';
import Toast from '../components/Toast';
import ConfirmDialog from '../components/ConfirmDialog';
import SimpleSearchInput from '../components/SimpleSearchInput';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
@ -836,12 +837,12 @@ const KnowledgeBasePage = ({ user }) => {
<div className="form-group">
{/* 紧凑的搜索和过滤区 */}
<div className="search-filter-area">
<input
type="text"
placeholder="搜索会议名称或创建人..."
<SimpleSearchInput
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="compact-search-input"
onChange={setSearchQuery}
placeholder="搜索会议名称或创建人..."
realTimeSearch={true}
debounceDelay={500}
/>
{availableTags.length > 0 && (

View File

@ -1526,8 +1526,8 @@
font-size: 0.8rem;
min-width: 80px;
}
.action-btn span {
.action-btn.icon-only span {
display: none;
}

View File

@ -150,6 +150,25 @@
margin-bottom: 40px;
}
/* Tab 样式 */
.preview-tabs {
margin-top: 20px;
}
.preview-tabs .ant-tabs-nav {
margin-bottom: 20px;
}
.preview-tabs .ant-tabs-tab {
font-size: 16px;
font-weight: 500;
padding: 12px 24px;
}
.preview-tabs .ant-tabs-tab-active {
font-weight: 600;
}
.summary-content {
font-size: 15px;
line-height: 1.8;
@ -157,6 +176,48 @@
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
padding: 20px 0;
}
/* 思维导图容器样式 */
.mindmap-wrapper {
width: 100%;
min-height: 500px;
height: 600px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.mindmap-wrapper .mindmap-container {
width: 100%;
height: 100%;
}
.mindmap-wrapper .markmap-render-area {
width: 100%;
height: 100%;
background: white;
}
.mindmap-wrapper .mindmap-loading,
.mindmap-wrapper .mindmap-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #64748b;
}
.mindmap-wrapper .mindmap-loading svg {
animation: spin 1s linear infinite;
}
.mindmap-wrapper .mindmap-error {
color: #dc2626;
}
/* Markdown 样式 */
@ -333,8 +394,40 @@
font-size: 14px;
}
/* Tab 移动端优化 */
.preview-tabs .ant-tabs-nav {
margin-bottom: 15px;
}
.preview-tabs .ant-tabs-nav-list {
width: 100%;
display: flex;
}
.preview-tabs .ant-tabs-tab {
font-size: 15px;
padding: 12px 20px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin: 0 !important;
}
.preview-tabs .ant-tabs-tab + .ant-tabs-tab {
margin: 0 !important;
}
/* 思维导图移动端优化 */
.mindmap-wrapper {
min-height: 400px;
height: 500px;
border-radius: 6px;
}
.summary-content {
font-size: 14px;
padding: 15px 0;
}
.summary-content h1 {
@ -385,6 +478,11 @@
.preview-title {
font-size: 24px;
}
/* 思维导图平板优化 */
.mindmap-wrapper {
height: 550px;
}
}
/* 打印样式优化 */

View File

@ -4,8 +4,12 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import { Tabs } from 'antd';
import MindMap from '../components/MindMap';
import './MeetingPreview.css';
const { TabPane } = Tabs;
const MeetingPreview = () => {
const { meeting_id } = useParams();
const [meetingData, setMeetingData] = useState(null);
@ -20,8 +24,8 @@ const MeetingPreview = () => {
const fetchMeetingPreviewData = async () => {
try {
setLoading(true);
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const response = await fetch(`${apiUrl}/api/meetings/${meeting_id}/preview-data`);
// Use relative path to work in both dev and production
const response = await fetch(`/api/meetings/${meeting_id}/preview-data`);
const result = await response.json();
if (result.code === "200") {
@ -132,15 +136,28 @@ const MeetingPreview = () => {
</div>
<div className="summary-section">
<h2 className="section-title">📝 会议摘要</h2>
<div className="summary-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{meetingData.summary}
</ReactMarkdown>
</div>
<h2 className="section-title">📝 会议内容</h2>
<Tabs defaultActiveKey="summary" className="preview-tabs">
<TabPane tab="摘要" key="summary">
<div className="summary-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{meetingData.summary}
</ReactMarkdown>
</div>
</TabPane>
<TabPane tab="脑图" key="mindmap">
<div className="mindmap-wrapper">
<MindMap
content={meetingData.summary}
title={meetingData.title}
initialScale={1.8}
/>
</div>
</TabPane>
</Tabs>
</div>
<div className="preview-footer">