2025-08-26 14:03:14 +00:00
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
2025-08-05 01:44:28 +00:00
|
|
|
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
|
|
|
|
|
import axios from 'axios';
|
2025-08-06 07:06:25 +00:00
|
|
|
|
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Image } from 'lucide-react';
|
|
|
|
|
|
import MDEditor, * as commands from '@uiw/react-md-editor';
|
2025-08-05 02:58:13 +00:00
|
|
|
|
import '@uiw/react-md-editor/markdown-editor.css';
|
2025-08-06 07:06:25 +00:00
|
|
|
|
import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api';
|
2025-08-26 13:59:15 +00:00
|
|
|
|
import DateTimePicker from '../components/DateTimePicker';
|
2025-08-05 01:44:28 +00:00
|
|
|
|
import './EditMeeting.css';
|
|
|
|
|
|
|
|
|
|
|
|
const EditMeeting = ({ user }) => {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const { meeting_id } = useParams();
|
2025-08-06 07:06:25 +00:00
|
|
|
|
const imageInputRef = useRef(null);
|
2025-08-05 01:44:28 +00:00
|
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
meeting_time: '',
|
|
|
|
|
|
summary: '',
|
|
|
|
|
|
attendees: []
|
|
|
|
|
|
});
|
|
|
|
|
|
const [availableUsers, setAvailableUsers] = useState([]);
|
|
|
|
|
|
const [userSearch, setUserSearch] = useState('');
|
|
|
|
|
|
const [showUserDropdown, setShowUserDropdown] = useState(false);
|
2025-08-05 02:58:13 +00:00
|
|
|
|
const [audioFile, setAudioFile] = useState(null);
|
2025-08-05 01:44:28 +00:00
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
2025-08-05 02:58:13 +00:00
|
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
2025-08-06 07:06:25 +00:00
|
|
|
|
const [isUploadingImage, setIsUploadingImage] = useState(false);
|
2025-08-05 01:44:28 +00:00
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
|
const [meeting, setMeeting] = useState(null);
|
2025-08-05 03:39:55 +00:00
|
|
|
|
const [showUploadArea, setShowUploadArea] = useState(false);
|
2025-08-28 08:02:34 +00:00
|
|
|
|
const [showUploadConfirm, setShowUploadConfirm] = useState(false);
|
2025-08-05 01:44:28 +00:00
|
|
|
|
|
2025-08-26 14:03:14 +00:00
|
|
|
|
const handleSummaryChange = useCallback((value) => {
|
|
|
|
|
|
setFormData(prev => ({ ...prev, summary: value || '' }));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-08-05 01:44:28 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchMeetingData();
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
}, [meeting_id]);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchMeetingData = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(buildApiUrl(API_ENDPOINTS.MEETINGS.EDIT(meeting_id)));
|
|
|
|
|
|
const meetingData = response.data;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if current user is the creator
|
|
|
|
|
|
if (meetingData.creator_id !== user.user_id) {
|
|
|
|
|
|
navigate('/dashboard');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setMeeting(meetingData);
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
title: meetingData.title,
|
2025-08-26 13:59:15 +00:00
|
|
|
|
meeting_time: meetingData.meeting_time || '',
|
2025-08-05 01:44:28 +00:00
|
|
|
|
summary: meetingData.summary || '',
|
|
|
|
|
|
attendees: meetingData.attendees || []
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError('无法加载会议信息');
|
|
|
|
|
|
console.error('Error fetching meeting:', err);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchUsers = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
|
|
|
|
|
|
setAvailableUsers(response.data.filter(u => u.user_id !== user.user_id));
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error fetching users:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleInputChange = (e) => {
|
|
|
|
|
|
const { name, value } = e.target;
|
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[name]: value
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleAddAttendee = (selectedUser) => {
|
|
|
|
|
|
if (!formData.attendees.find(a => a.user_id === selectedUser.user_id)) {
|
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
attendees: [...prev.attendees, selectedUser]
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
setUserSearch('');
|
|
|
|
|
|
setShowUserDropdown(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRemoveAttendee = (userId) => {
|
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
attendees: prev.attendees.filter(a => a.user_id !== userId)
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-05 02:58:13 +00:00
|
|
|
|
const handleFileChange = (e) => {
|
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
|
if (file) {
|
|
|
|
|
|
// Check file type - include both MIME types and extensions
|
|
|
|
|
|
const allowedMimeTypes = ['audio/mp3', 'audio/wav', 'audio/m4a', 'audio/mpeg', 'audio/mp4', 'audio/x-m4a'];
|
|
|
|
|
|
const fileExtension = file.name.toLowerCase().split('.').pop();
|
|
|
|
|
|
const allowedExtensions = ['mp3', 'wav', 'm4a', 'mpeg'];
|
|
|
|
|
|
|
|
|
|
|
|
if (!allowedMimeTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) {
|
|
|
|
|
|
setError('请上传支持的音频格式 (MP3, WAV, M4A)');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Check file size (max 100MB)
|
|
|
|
|
|
if (file.size > 100 * 1024 * 1024) {
|
|
|
|
|
|
setError('音频文件大小不能超过100MB');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setAudioFile(file);
|
|
|
|
|
|
setError('');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-05 01:44:28 +00:00
|
|
|
|
const handleSubmit = async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (!formData.title.trim()) {
|
|
|
|
|
|
setError('请输入会议标题');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updateData = {
|
|
|
|
|
|
title: formData.title,
|
|
|
|
|
|
meeting_time: formData.meeting_time || null,
|
|
|
|
|
|
summary: formData.summary,
|
|
|
|
|
|
attendee_ids: formData.attendees.map(a => a.user_id)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await axios.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), updateData);
|
|
|
|
|
|
navigate(`/meetings/${meeting_id}`);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err.response?.data?.detail || '更新会议失败,请重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-05 02:58:13 +00:00
|
|
|
|
const handleUploadAudio = async () => {
|
|
|
|
|
|
if (!audioFile) {
|
|
|
|
|
|
setError('请先选择音频文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsUploading(true);
|
2025-08-05 01:44:28 +00:00
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-05 02:58:13 +00:00
|
|
|
|
const formDataUpload = new FormData();
|
|
|
|
|
|
formDataUpload.append('audio_file', audioFile);
|
|
|
|
|
|
formDataUpload.append('meeting_id', meeting_id);
|
2025-08-28 08:02:34 +00:00
|
|
|
|
formDataUpload.append('force_replace', 'true'); // Always force replace in edit mode
|
2025-08-05 02:58:13 +00:00
|
|
|
|
|
|
|
|
|
|
const response = await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'multipart/form-data',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setAudioFile(null);
|
2025-08-05 03:39:55 +00:00
|
|
|
|
setShowUploadArea(false);
|
2025-08-28 08:02:34 +00:00
|
|
|
|
setShowUploadConfirm(false);
|
2025-08-05 02:58:13 +00:00
|
|
|
|
// Reset file input
|
|
|
|
|
|
const fileInput = document.getElementById('audio-file');
|
|
|
|
|
|
if (fileInput) fileInput.value = '';
|
|
|
|
|
|
|
2025-08-05 01:44:28 +00:00
|
|
|
|
} catch (err) {
|
2025-08-28 08:02:34 +00:00
|
|
|
|
console.error('Upload error:', err);
|
|
|
|
|
|
setError(err.response?.data?.detail || '上传音频文件失败,请重试');
|
2025-08-05 01:44:28 +00:00
|
|
|
|
} finally {
|
2025-08-05 02:58:13 +00:00
|
|
|
|
setIsUploading(false);
|
2025-08-05 01:44:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-06 07:06:25 +00:00
|
|
|
|
const handleImageUpload = async (file) => {
|
|
|
|
|
|
if (!file) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// Validate file type
|
|
|
|
|
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
|
|
|
|
|
if (!allowedTypes.includes(file.type)) {
|
|
|
|
|
|
setError('请上传支持的图片格式 (JPG, PNG, GIF, WebP)');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate file size (10MB)
|
|
|
|
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
|
|
|
|
setError('图片大小不能超过10MB');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsUploadingImage(true);
|
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('image_file', file);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await axios.post(
|
|
|
|
|
|
buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)),
|
|
|
|
|
|
formData,
|
|
|
|
|
|
{
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'multipart/form-data',
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-08-25 03:30:27 +00:00
|
|
|
|
return `${API_BASE_URL}${response.data.file_path}`;
|
2025-08-06 07:06:25 +00:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError('上传图片失败,请重试');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsUploadingImage(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const insertImageMarkdown = (imageUrl, altText = '图片') => {
|
|
|
|
|
|
const imageMarkdown = ``;
|
|
|
|
|
|
setFormData(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
summary: prev.summary + '\n\n' + imageMarkdown
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleImageSelect = async (event) => {
|
|
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
|
if (file) {
|
|
|
|
|
|
const imageUrl = await handleImageUpload(file);
|
|
|
|
|
|
if (imageUrl) {
|
|
|
|
|
|
insertImageMarkdown(imageUrl, file.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Reset file input
|
|
|
|
|
|
if (imageInputRef.current) {
|
|
|
|
|
|
imageInputRef.current.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建自定义上传图片命令(只显示文字,不显示图标)
|
|
|
|
|
|
const uploadImageCommand = {
|
|
|
|
|
|
name: 'upload-image',
|
|
|
|
|
|
keyCommand: 'upload-image',
|
|
|
|
|
|
buttonProps: { 'aria-label': '上传本地图片', title: '上传本地图片' },
|
|
|
|
|
|
icon: <span style={{fontSize:12}}>UploadImage</span>,
|
|
|
|
|
|
execute: () => {
|
|
|
|
|
|
imageInputRef.current?.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建修改后的图片URL命令
|
|
|
|
|
|
const imageUrlCommand = {
|
|
|
|
|
|
...commands.image,
|
|
|
|
|
|
name: 'image-url',
|
|
|
|
|
|
icon: <span style={{fontSize:12}}>AddImageURL</span>,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义工具栏命令配置
|
|
|
|
|
|
const customCommands = [
|
|
|
|
|
|
commands.bold,
|
|
|
|
|
|
commands.italic,
|
|
|
|
|
|
commands.strikethrough,
|
|
|
|
|
|
commands.hr,
|
|
|
|
|
|
commands.group([
|
|
|
|
|
|
commands.title1,
|
|
|
|
|
|
commands.title2,
|
|
|
|
|
|
commands.title3,
|
|
|
|
|
|
commands.title4,
|
|
|
|
|
|
commands.title5,
|
|
|
|
|
|
commands.title6,
|
|
|
|
|
|
], {
|
|
|
|
|
|
name: 'title',
|
|
|
|
|
|
groupName: 'title',
|
|
|
|
|
|
buttonProps: { 'aria-label': '插入标题', title: '插入标题' }
|
|
|
|
|
|
}),
|
|
|
|
|
|
commands.divider,
|
|
|
|
|
|
commands.link,
|
|
|
|
|
|
commands.quote,
|
|
|
|
|
|
commands.code,
|
|
|
|
|
|
commands.codeBlock,
|
|
|
|
|
|
// 创建图片功能组,使用系统自带的image命令和自定义上传命令
|
|
|
|
|
|
commands.group([
|
|
|
|
|
|
imageUrlCommand,
|
|
|
|
|
|
uploadImageCommand
|
|
|
|
|
|
], {
|
|
|
|
|
|
name: 'image-group',
|
|
|
|
|
|
groupName: 'image',
|
|
|
|
|
|
buttonProps: { 'aria-label': '添加图片', title: '添加图片' },
|
|
|
|
|
|
icon: (
|
|
|
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
)
|
|
|
|
|
|
}),
|
|
|
|
|
|
commands.divider,
|
|
|
|
|
|
commands.unorderedListCommand,
|
|
|
|
|
|
commands.orderedListCommand,
|
|
|
|
|
|
commands.checkedListCommand,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 右侧额外命令(预览、全屏等)
|
|
|
|
|
|
const customExtraCommands = [
|
|
|
|
|
|
commands.codeEdit,
|
|
|
|
|
|
commands.codeLive,
|
|
|
|
|
|
commands.codePreview,
|
|
|
|
|
|
commands.divider,
|
|
|
|
|
|
commands.fullscreen,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-08-05 01:44:28 +00:00
|
|
|
|
const filteredUsers = availableUsers.filter(user => {
|
|
|
|
|
|
// Exclude users already selected as attendees
|
|
|
|
|
|
const isAlreadySelected = formData.attendees.some(attendee => attendee.user_id === user.user_id);
|
|
|
|
|
|
if (isAlreadySelected) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Filter by search text
|
|
|
|
|
|
return user.caption.toLowerCase().includes(userSearch.toLowerCase()) ||
|
|
|
|
|
|
user.username.toLowerCase().includes(userSearch.toLowerCase());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="edit-meeting-page">
|
|
|
|
|
|
<div className="loading-container">
|
|
|
|
|
|
<div className="loading-spinner"></div>
|
|
|
|
|
|
<p>加载中...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="edit-meeting-page">
|
|
|
|
|
|
<div className="edit-header">
|
|
|
|
|
|
<Link to="/dashboard">
|
|
|
|
|
|
<span className="back-link">
|
|
|
|
|
|
<ArrowLeft size={20} />
|
|
|
|
|
|
<span>返回首页</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="edit-content">
|
|
|
|
|
|
<div className="edit-card">
|
|
|
|
|
|
<header className="edit-card-header">
|
|
|
|
|
|
<h1>编辑会议纪要</h1>
|
|
|
|
|
|
<p>修改会议信息、参会人员和摘要内容</p>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="edit-form">
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
|
<label htmlFor="title">
|
|
|
|
|
|
<FileText size={18} />
|
|
|
|
|
|
会议标题 *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
id="title"
|
|
|
|
|
|
name="title"
|
|
|
|
|
|
value={formData.title}
|
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
|
placeholder="请输入会议标题"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="form-group">
|
2025-08-26 13:59:15 +00:00
|
|
|
|
<label>
|
2025-08-05 01:44:28 +00:00
|
|
|
|
<Calendar size={18} />
|
|
|
|
|
|
会议时间
|
|
|
|
|
|
</label>
|
2025-08-26 13:59:15 +00:00
|
|
|
|
<DateTimePicker
|
2025-08-05 01:44:28 +00:00
|
|
|
|
value={formData.meeting_time}
|
2025-08-26 13:59:15 +00:00
|
|
|
|
onChange={(value) => setFormData(prev => ({ ...prev, meeting_time: value }))}
|
|
|
|
|
|
placeholder="选择会议时间(可选)"
|
2025-08-05 01:44:28 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<Users size={18} />
|
|
|
|
|
|
参会人员
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="attendees-container">
|
|
|
|
|
|
<div className="selected-attendees">
|
|
|
|
|
|
{formData.attendees.map(attendee => (
|
|
|
|
|
|
<div key={attendee.user_id} className="attendee-chip">
|
|
|
|
|
|
<User size={16} />
|
|
|
|
|
|
<span>{attendee.caption}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleRemoveAttendee(attendee.user_id)}
|
|
|
|
|
|
className="remove-attendee"
|
|
|
|
|
|
title="移除参会人"
|
|
|
|
|
|
>
|
2025-08-05 02:58:13 +00:00
|
|
|
|
<X size={14} />
|
2025-08-05 01:44:28 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="user-search-container">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={userSearch}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
setUserSearch(e.target.value);
|
|
|
|
|
|
setShowUserDropdown(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onFocus={() => setShowUserDropdown(true)}
|
|
|
|
|
|
onBlur={() => {
|
|
|
|
|
|
// Delay hiding dropdown to allow click events
|
|
|
|
|
|
setTimeout(() => setShowUserDropdown(false), 200);
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="搜索用户名或姓名添加参会人..."
|
|
|
|
|
|
className="user-search-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{showUserDropdown && userSearch && (
|
|
|
|
|
|
<div className="user-dropdown">
|
|
|
|
|
|
{filteredUsers.length > 0 ? (
|
|
|
|
|
|
filteredUsers.map(user => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={user.user_id}
|
|
|
|
|
|
className="user-option"
|
|
|
|
|
|
onClick={() => handleAddAttendee(user)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<User size={16} />
|
|
|
|
|
|
<div className="user-info">
|
|
|
|
|
|
<span className="user-name">{user.caption}</span>
|
|
|
|
|
|
<span className="user-username">@{user.username}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="no-users">未找到匹配的用户</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-05 02:58:13 +00:00
|
|
|
|
<div className="form-group">
|
2025-08-05 03:39:55 +00:00
|
|
|
|
<div className="audio-upload-section">
|
|
|
|
|
|
{!showUploadArea ? (
|
2025-08-05 02:58:13 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2025-08-05 03:39:55 +00:00
|
|
|
|
onClick={() => setShowUploadArea(true)}
|
|
|
|
|
|
className="show-upload-btn"
|
2025-08-05 02:58:13 +00:00
|
|
|
|
>
|
|
|
|
|
|
<Upload size={16} />
|
2025-08-05 03:39:55 +00:00
|
|
|
|
<span>重新上传录音文件</span>
|
2025-08-05 02:58:13 +00:00
|
|
|
|
</button>
|
2025-08-05 03:39:55 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="file-upload-container">
|
|
|
|
|
|
<div className="upload-header">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setShowUploadArea(false);
|
|
|
|
|
|
setAudioFile(null);
|
|
|
|
|
|
// Reset file input
|
|
|
|
|
|
const fileInput = document.getElementById('audio-file');
|
|
|
|
|
|
if (fileInput) fileInput.value = '';
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="close-upload-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X size={16} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
id="audio-file"
|
|
|
|
|
|
accept="audio/*"
|
|
|
|
|
|
onChange={handleFileChange}
|
|
|
|
|
|
className="file-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label htmlFor="audio-file" className="file-upload-label">
|
|
|
|
|
|
<Plus size={20} />
|
|
|
|
|
|
<span>选择新的音频文件</span>
|
|
|
|
|
|
<small>支持 MP3, WAV, M4A 格式,最大100MB</small>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
{audioFile && (
|
|
|
|
|
|
<div className="selected-file">
|
|
|
|
|
|
<span>已选择: {audioFile.name}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setAudioFile(null)}
|
|
|
|
|
|
className="remove-file"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X size={16} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{audioFile && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2025-08-28 08:02:34 +00:00
|
|
|
|
onClick={() => setShowUploadConfirm(true)}
|
2025-08-05 03:39:55 +00:00
|
|
|
|
className="upload-btn"
|
|
|
|
|
|
disabled={isUploading}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Upload size={16} />
|
|
|
|
|
|
{isUploading ? '上传并分析中...' : '上传并重新分析'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-08-05 02:58:13 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-05 01:44:28 +00:00
|
|
|
|
<div className="form-group">
|
|
|
|
|
|
<div className="summary-header">
|
|
|
|
|
|
<label htmlFor="summary">
|
|
|
|
|
|
<FileText size={18} />
|
|
|
|
|
|
会议摘要
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2025-08-05 02:58:13 +00:00
|
|
|
|
<div className="markdown-editor-container">
|
|
|
|
|
|
<MDEditor
|
2025-08-26 14:03:14 +00:00
|
|
|
|
key="summary-editor"
|
2025-08-05 02:58:13 +00:00
|
|
|
|
value={formData.summary}
|
2025-08-26 14:03:14 +00:00
|
|
|
|
onChange={handleSummaryChange}
|
2025-08-05 02:58:13 +00:00
|
|
|
|
data-color-mode="light"
|
|
|
|
|
|
height={400}
|
|
|
|
|
|
preview="edit"
|
|
|
|
|
|
hideToolbar={false}
|
|
|
|
|
|
toolbarBottom={false}
|
2025-08-06 07:06:25 +00:00
|
|
|
|
commands={customCommands}
|
|
|
|
|
|
extraCommands={customExtraCommands}
|
2025-08-26 14:03:14 +00:00
|
|
|
|
autoFocus={false}
|
|
|
|
|
|
textareaProps={{
|
|
|
|
|
|
placeholder: '在这里编写会议摘要...',
|
|
|
|
|
|
style: {
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
lineHeight: '1.5',
|
|
|
|
|
|
fontFamily: 'inherit'
|
|
|
|
|
|
},
|
|
|
|
|
|
spellCheck: false,
|
|
|
|
|
|
autoComplete: 'off',
|
|
|
|
|
|
autoCapitalize: 'off',
|
|
|
|
|
|
autoCorrect: 'off'
|
|
|
|
|
|
}}
|
2025-08-06 07:06:25 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref={imageInputRef}
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
onChange={handleImageSelect}
|
|
|
|
|
|
style={{ display: 'none' }}
|
2025-08-05 02:58:13 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-05 01:44:28 +00:00
|
|
|
|
<div className="markdown-hint">
|
2025-08-06 07:06:25 +00:00
|
|
|
|
<small>使用Markdown格式编写会议摘要,支持**粗体**、*斜体*、# 标题、- 列表等格式。工具栏中可以上传图片或插入图片URL。</small>
|
2025-08-05 01:44:28 +00:00
|
|
|
|
</div>
|
2025-08-06 07:06:25 +00:00
|
|
|
|
{isUploadingImage && (
|
|
|
|
|
|
<div className="uploading-indicator">
|
|
|
|
|
|
<span>正在上传图片...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-08-05 01:44:28 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
|
<div className="error-message">{error}</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="form-actions">
|
|
|
|
|
|
<Link to={`/meetings/${meeting_id}`}>
|
|
|
|
|
|
<span className="btn-cancel">取消</span>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
className="btn-submit"
|
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Save size={16} />
|
|
|
|
|
|
{isSaving ? '保存中...' : '保存更改'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-28 08:02:34 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Upload Confirmation Modal */}
|
|
|
|
|
|
{showUploadConfirm && (
|
|
|
|
|
|
<div className="delete-modal-overlay" onClick={() => setShowUploadConfirm(false)}>
|
|
|
|
|
|
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<h3>确认重新上传</h3>
|
|
|
|
|
|
<p>重传音频文件将清空已有的会话转录,是否继续?</p>
|
|
|
|
|
|
<div className="modal-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="btn-cancel"
|
|
|
|
|
|
onClick={() => setShowUploadConfirm(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="btn-submit"
|
|
|
|
|
|
onClick={handleUploadAudio}
|
|
|
|
|
|
disabled={isUploading}
|
|
|
|
|
|
>
|
|
|
|
|
|
确定重传
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-08-05 01:44:28 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default EditMeeting;
|