修复了前端的问题
parent
06f1b959b4
commit
e5b04ed2d6
Binary file not shown.
|
|
@ -77,6 +77,7 @@ const ContentViewer = ({
|
|||
<MindMap
|
||||
content={content}
|
||||
title={title}
|
||||
initialScale={1.8}
|
||||
/>
|
||||
) : (
|
||||
<div className="empty-content">等待内容生成后查看脑图</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -1526,8 +1526,8 @@
|
|||
font-size: 0.8rem;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.action-btn span {
|
||||
|
||||
.action-btn.icon-only span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 打印样式优化 */
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue