328 lines
10 KiB
React
328 lines
10 KiB
React
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
|||
|
|
import axios from 'axios';
|
|||
|
|
import { ArrowLeft, Users, Calendar, FileText, X, User, Save, Zap } from 'lucide-react';
|
|||
|
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
|||
|
|
import './EditMeeting.css';
|
|||
|
|
|
|||
|
|
const EditMeeting = ({ user }) => {
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
const { meeting_id } = useParams();
|
|||
|
|
const [formData, setFormData] = useState({
|
|||
|
|
title: '',
|
|||
|
|
meeting_time: '',
|
|||
|
|
summary: '',
|
|||
|
|
attendees: []
|
|||
|
|
});
|
|||
|
|
const [availableUsers, setAvailableUsers] = useState([]);
|
|||
|
|
const [userSearch, setUserSearch] = useState('');
|
|||
|
|
const [showUserDropdown, setShowUserDropdown] = useState(false);
|
|||
|
|
const [isLoading, setIsLoading] = useState(true);
|
|||
|
|
const [isSaving, setIsSaving] = useState(false);
|
|||
|
|
const [isRegenerating, setIsRegenerating] = useState(false);
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
const [meeting, setMeeting] = useState(null);
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
meeting_time: meetingData.meeting_time ?
|
|||
|
|
new Date(meetingData.meeting_time).toISOString().slice(0, 16) : '',
|
|||
|
|
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)
|
|||
|
|
}));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRegenerateSummary = async () => {
|
|||
|
|
setIsRegenerating(true);
|
|||
|
|
setError('');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await axios.post(buildApiUrl(API_ENDPOINTS.MEETINGS.REGENERATE_SUMMARY(meeting_id)));
|
|||
|
|
setFormData(prev => ({
|
|||
|
|
...prev,
|
|||
|
|
summary: response.data.summary
|
|||
|
|
}));
|
|||
|
|
} catch (err) {
|
|||
|
|
setError('重新生成摘要失败,请重试');
|
|||
|
|
} finally {
|
|||
|
|
setIsRegenerating(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 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="移除参会人"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</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>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={handleRegenerateSummary}
|
|||
|
|
className="regenerate-btn"
|
|||
|
|
disabled={isRegenerating}
|
|||
|
|
>
|
|||
|
|
<Zap size={16} />
|
|||
|
|
{isRegenerating ? 'AI生成中...' : '重新AI解析'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<textarea
|
|||
|
|
id="summary"
|
|||
|
|
name="summary"
|
|||
|
|
value={formData.summary}
|
|||
|
|
onChange={handleInputChange}
|
|||
|
|
placeholder="会议摘要将显示在这里... 支持Markdown格式"
|
|||
|
|
rows="12"
|
|||
|
|
className="summary-textarea"
|
|||
|
|
/>
|
|||
|
|
<div className="markdown-hint">
|
|||
|
|
<small>支持Markdown格式:**粗体** *斜体* # 标题 - 列表</small>
|
|||
|
|
</div>
|
|||
|
|
</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>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default EditMeeting;
|