diff --git a/src/components/MindMap.jsx b/src/components/MindMap.jsx
index 53a89ac..93d4c7a 100644
--- a/src/components/MindMap.jsx
+++ b/src/components/MindMap.jsx
@@ -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();
- // 在fit之后,再放大1.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 (
diff --git a/src/components/SimpleSearchInput.css b/src/components/SimpleSearchInput.css
new file mode 100644
index 0000000..76747b0
--- /dev/null
+++ b/src/components/SimpleSearchInput.css
@@ -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;
+}
diff --git a/src/components/SimpleSearchInput.jsx b/src/components/SimpleSearchInput.jsx
new file mode 100644
index 0000000..90f46f9
--- /dev/null
+++ b/src/components/SimpleSearchInput.jsx
@@ -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 (
+
+
+ {localValue && (
+
+ )}
+
+ );
+};
+
+export default SimpleSearchInput;
diff --git a/src/pages/Dashboard.css b/src/pages/Dashboard.css
index bcb022b..c7c41d5 100644
--- a/src/pages/Dashboard.css
+++ b/src/pages/Dashboard.css
@@ -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;
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 21aa748..0a726ed 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -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 }) => {
iMeeting
-
-
setDropdownOpen(!dropdownOpen)}>
- 欢迎,{userInfo?.caption}
-
-
- {dropdownOpen && (
-
-
- setDropdownOpen(false)}> 提示词仓库
- {user.role_id === 1 && (
- setDropdownOpen(false)}> 平台管理
- )}
-
+
+ 欢迎,{userInfo?.caption}
+
- )}
-
+ }
+ items={[
+ {
+ icon:
,
+ label: '修改密码',
+ onClick: () => setShowChangePasswordModal(true)
+ },
+ {
+ icon:
,
+ label: '提示词仓库',
+ onClick: () => window.location.href = '/prompt-management'
+ },
+ ...(user.role_id === 1 ? [{
+ icon:
,
+ label: '平台管理',
+ onClick: () => window.location.href = '/admin/management'
+ }] : []),
+ {
+ icon:
,
+ label: '退出登录',
+ onClick: onLogout
+ }
+ ]}
+ align="right"
+ className="user-menu-dropdown"
+ />
@@ -484,13 +487,12 @@ const Dashboard = ({ user, onLogout }) => {
{/* 搜索和标签过滤卡片 */}
-
diff --git a/src/pages/KnowledgeBasePage.jsx b/src/pages/KnowledgeBasePage.jsx
index f542987..874eff0 100644
--- a/src/pages/KnowledgeBasePage.jsx
+++ b/src/pages/KnowledgeBasePage.jsx
@@ -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 }) => {
{/* 紧凑的搜索和过滤区 */}
- setSearchQuery(e.target.value)}
- className="compact-search-input"
+ onChange={setSearchQuery}
+ placeholder="搜索会议名称或创建人..."
+ realTimeSearch={true}
+ debounceDelay={500}
/>
{availableTags.length > 0 && (
diff --git a/src/pages/MeetingDetails.css b/src/pages/MeetingDetails.css
index 9ffa517..cef64a0 100644
--- a/src/pages/MeetingDetails.css
+++ b/src/pages/MeetingDetails.css
@@ -1526,8 +1526,8 @@
font-size: 0.8rem;
min-width: 80px;
}
-
- .action-btn span {
+
+ .action-btn.icon-only span {
display: none;
}
diff --git a/src/pages/MeetingPreview.css b/src/pages/MeetingPreview.css
index 96fbb28..6abf560 100644
--- a/src/pages/MeetingPreview.css
+++ b/src/pages/MeetingPreview.css
@@ -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;
+ }
}
/* 打印样式优化 */
diff --git a/src/pages/MeetingPreview.jsx b/src/pages/MeetingPreview.jsx
index cdabe06..7d2349e 100644
--- a/src/pages/MeetingPreview.jsx
+++ b/src/pages/MeetingPreview.jsx
@@ -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 = () => {
-
📝 会议摘要
-
-
- {meetingData.summary}
-
-
+
📝 会议内容
+
+
+
+
+ {meetingData.summary}
+
+
+
+
+
+
+
+
+