imetting_frontend/src/pages/CreateMeeting.jsx

307 lines
10 KiB
React
Raw Normal View History

2025-08-05 01:44:28 +00:00
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus } from 'lucide-react';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import './CreateMeeting.css';
const CreateMeeting = ({ user }) => {
const navigate = useNavigate();
const [formData, setFormData] = useState({
title: '',
meeting_time: '',
attendees: []
});
const [availableUsers, setAvailableUsers] = useState([]);
const [userSearch, setUserSearch] = useState('');
const [showUserDropdown, setShowUserDropdown] = useState(false);
const [audioFile, setAudioFile] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
fetchUsers();
}, []);
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)
}));
};
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('');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.title.trim()) {
setError('请输入会议标题');
return;
}
setIsLoading(true);
setError('');
try {
// Create meeting
const meetingData = {
title: formData.title,
meeting_time: formData.meeting_time || null,
attendee_ids: formData.attendees.map(a => a.user_id)
};
const response = await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), meetingData);
const meetingId = response.data.meeting_id;
// Upload audio file if provided
if (audioFile) {
const formDataUpload = new FormData();
formDataUpload.append('audio_file', audioFile);
formDataUpload.append('meeting_id', meetingId);
await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
navigate(`/meetings/${meetingId}`);
} catch (err) {
setError(err.response?.data?.detail || '创建会议失败,请重试');
} finally {
setIsLoading(false);
}
};
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());
});
return (
<div className="create-meeting-page">
<div className="create-header">
<Link to="/dashboard">
<span className="back-link">
<ArrowLeft size={20} />
<span>返回首页</span>
</span>
</Link>
</div>
<div className="create-content">
<div className="create-card">
<header className="create-card-header">
<h1>新建会议纪要</h1>
<p>创建新的会议记录并上传音频文件进行AI处理</p>
</header>
<form onSubmit={handleSubmit} className="create-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">
<label htmlFor="meeting_time">
<Calendar size={18} />
会议时间
</label>
<input
type="datetime-local"
id="meeting_time"
name="meeting_time"
value={formData.meeting_time}
onChange={handleInputChange}
onBlur={(e) => {
// Force input to lose focus to save the value
e.target.blur();
}}
/>
</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>
<div className="form-group">
<label>
<Upload size={18} />
会议录音
</label>
<div className="file-upload-container">
<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>
)}
</div>
</div>
{error && (
<div className="error-message">{error}</div>
)}
<div className="form-actions">
<Link to="/dashboard">
<span className="btn-cancel">取消</span>
</Link>
<button
type="submit"
className="btn-submit"
disabled={isLoading}
>
{isLoading ? '创建中...' : '创建会议'}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default CreateMeeting;