imetting_frontend/src/pages/CreateMeeting.jsx

332 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import configService from '../utils/configService';
import { ArrowLeft, Upload, Users, Calendar, FileText, X, User, Plus, Tag } from 'lucide-react';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import DateTimePicker from '../components/DateTimePicker';
import TagEditor from '../components/TagEditor';
import './CreateMeeting.css';
const CreateMeeting = ({ user }) => {
const navigate = useNavigate();
const [formData, setFormData] = useState({
title: '',
meeting_time: '',
attendees: [],
tags: ''
});
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('');
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 默认100MB
useEffect(() => {
fetchUsers();
loadMaxFileSize();
}, []);
const loadMaxFileSize = async () => {
try {
const size = await configService.getMaxFileSize();
setMaxFileSize(size);
} catch (error) {
console.warn('Failed to load max file size config:', error);
}
};
const fetchUsers = async () => {
try {
// 获取所有用户设置较大的size参数
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000`));
setAvailableUsers(response.data.users.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 using dynamic config
if (file.size > maxFileSize) {
const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
setError(`音频文件大小不能超过${maxSizeMB}MB`);
return;
}
setAudioFile(file);
setError('');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.title.trim()) {
setError('请输入会议标题');
return;
}
setIsLoading(true);
setError('');
try {
// Create meeting
const meetingData = {
user_id: user.user_id,
title: formData.title,
meeting_time: formData.meeting_time || null,
attendee_ids: formData.attendees.map(a => a.user_id),
tags: formData.tags
};
const response = await apiClient.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 apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formDataUpload, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
navigate(`/meetings/${meetingId}`);
} catch (err) {
setError(err.response?.data?.message || '创建会议失败,请重试');
} 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>
<Calendar size={18} />
会议时间
</label>
<DateTimePicker
value={formData.meeting_time}
onChange={(value) => setFormData(prev => ({ ...prev, meeting_time: value }))}
placeholder="选择会议时间(可选)"
/>
</div>
<div className="form-group">
<label htmlFor="tags">
<Tag size={18} />
会议标签
</label>
<TagEditor
value={formData.tags}
onChange={(value) => setFormData(prev => ({ ...prev, tags: value }))}
placeholder="输入标签,按回车或逗号分隔"
/>
</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="移除参会人"
>
<X size={14} />
</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 格式</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;