332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
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; |