imetting/frontend/src/pages/EditMeeting.jsx

373 lines
12 KiB
React
Raw Normal View History

import React, { useState, useEffect, useRef } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import configService from '../utils/configService';
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Upload, Plus, Tag } from 'lucide-react';
import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api';
import DateTimePicker from '../components/DateTimePicker';
import TagEditor from '../components/TagEditor';
import MarkdownEditor from '../components/MarkdownEditor';
import './EditMeeting.css';
const EditMeeting = ({ user }) => {
const navigate = useNavigate();
const { meeting_id } = useParams();
const [formData, setFormData] = useState({
title: '',
meeting_time: '',
summary: '',
attendees: [],
tags: ''
});
const [availableUsers, setAvailableUsers] = useState([]);
const [userSearch, setUserSearch] = useState('');
const [showUserDropdown, setShowUserDropdown] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isUploadingImage, setIsUploadingImage] = useState(false);
const [error, setError] = useState('');
const [meeting, setMeeting] = useState(null);
const [maxImageSize, setMaxImageSize] = useState(10 * 1024 * 1024); // 默认10MB
useEffect(() => {
fetchMeetingData();
fetchUsers();
loadFileSizeConfig();
}, [meeting_id]);
const loadFileSizeConfig = async () => {
try {
const imageSize = await configService.getMaxImageSize();
setMaxImageSize(imageSize);
} catch (error) {
console.warn('Failed to load file size config:', error);
}
};
const fetchMeetingData = async () => {
try {
const response = await apiClient.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,
meeting_time: meetingData.meeting_time || '',
summary: meetingData.summary || '',
attendees: meetingData.attendees || [],
tags: meetingData.tags ? meetingData.tags.map(t => t.name).join(', ') : ''
});
} catch (err) {
setError('无法加载会议信息');
console.error('Error fetching meeting:', err);
} finally {
setIsLoading(false);
}
};
const fetchUsers = async () => {
try {
// 获取所有普通用户role_id=2设置较大的size参数
const response = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.USERS.LIST}?page=1&size=1000&role_id=2`));
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 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),
tags: formData.tags
};
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), updateData);
navigate(`/meetings/${meeting_id}`);
} catch (err) {
setError(err.response?.data?.message || '更新会议失败,请重试');
} finally {
setIsSaving(false);
}
};
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 using dynamic config
if (file.size > maxImageSize) {
const maxSizeMB = Math.round(maxImageSize / (1024 * 1024));
setError(`图片大小不能超过${maxSizeMB}MB`);
return null;
}
setIsUploadingImage(true);
setError('');
try {
const formData = new FormData();
formData.append('image_file', file);
const response = await apiClient.post(
buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_IMAGE(meeting_id)),
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
return `${API_BASE_URL}${response.data.file_path}`;
} catch (err) {
setError('上传图片失败,请重试');
return null;
} finally {
setIsUploadingImage(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());
});
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">
<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">
<div className="summary-header">
<label htmlFor="summary">
<FileText size={18} />
会议摘要
</label>
</div>
<MarkdownEditor
value={formData.summary}
onChange={(value) => setFormData(prev => ({ ...prev, summary: value || '' }))}
onImageUpload={handleImageUpload}
placeholder="在这里编写会议摘要..."
height={400}
showImageUpload={true}
/>
<div className="markdown-hint">
<small>使用Markdown格式编写会议摘要支持**粗体***斜体*# 标题- 列表表格等格式</small>
</div>
{isUploadingImage && (
<div className="uploading-indicator">
<span>正在上传图片...</span>
</div>
)}
</div>
{error && !showUploadArea && (
<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>
</div>
);
};
export default EditMeeting;