309 lines
12 KiB
JavaScript
309 lines
12 KiB
JavaScript
import React, { useCallback, useEffect, useState } from 'react';
|
||
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App, Upload, Card, Progress, Typography } from 'antd';
|
||
import { SaveOutlined, UploadOutlined, DeleteOutlined, AudioOutlined } from '@ant-design/icons';
|
||
import dayjs from 'dayjs';
|
||
import apiClient from '../utils/apiClient';
|
||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||
import configService from '../utils/configService';
|
||
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||
|
||
const { Text } = Typography;
|
||
|
||
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||
const { message } = App.useApp();
|
||
const [form] = Form.useForm();
|
||
const [loading, setLoading] = useState(false);
|
||
const [users, setUsers] = useState([]);
|
||
const [prompts, setPrompts] = useState([]);
|
||
const [selectedAudioFile, setSelectedAudioFile] = useState(null);
|
||
const [audioUploading, setAudioUploading] = useState(false);
|
||
const [audioUploadProgress, setAudioUploadProgress] = useState(0);
|
||
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
||
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||
|
||
const isEdit = Boolean(meetingId);
|
||
|
||
const fetchOptions = useCallback(async () => {
|
||
try {
|
||
const [uRes, pRes] = await Promise.all([
|
||
apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.LIST)),
|
||
apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK'))),
|
||
]);
|
||
setUsers(uRes.data.users || []);
|
||
setPrompts(pRes.data.prompts || []);
|
||
} catch {
|
||
message.error('加载会议表单选项失败');
|
||
}
|
||
}, [message]);
|
||
|
||
const loadAudioUploadConfig = useCallback(async () => {
|
||
try {
|
||
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||
} catch {
|
||
setMaxAudioSize(100 * 1024 * 1024);
|
||
}
|
||
}, []);
|
||
|
||
const fetchMeeting = useCallback(async () => {
|
||
try {
|
||
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.DETAIL(meetingId)));
|
||
const meeting = res.data;
|
||
form.setFieldsValue({
|
||
title: meeting.title,
|
||
meeting_time: dayjs(meeting.meeting_time),
|
||
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
||
prompt_id: meeting.prompt_id,
|
||
tags: meeting.tags?.map((t) => t.name) || [],
|
||
});
|
||
} catch {
|
||
message.error('加载会议数据失败');
|
||
}
|
||
}, [form, meetingId, message]);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
fetchOptions();
|
||
loadAudioUploadConfig();
|
||
if (isEdit) {
|
||
fetchMeeting();
|
||
} else {
|
||
form.resetFields();
|
||
form.setFieldsValue({ meeting_time: dayjs() });
|
||
setSelectedAudioFile(null);
|
||
setAudioUploading(false);
|
||
setAudioUploadProgress(0);
|
||
setAudioUploadMessage('');
|
||
}
|
||
}, [fetchMeeting, fetchOptions, form, isEdit, loadAudioUploadConfig, open]);
|
||
|
||
const handleAudioBeforeUpload = (file) => {
|
||
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||
if (validationMessage) {
|
||
message.warning(validationMessage);
|
||
return Upload.LIST_IGNORE;
|
||
}
|
||
|
||
setSelectedAudioFile(file);
|
||
return false;
|
||
};
|
||
|
||
const clearSelectedAudio = () => {
|
||
setSelectedAudioFile(null);
|
||
setAudioUploadProgress(0);
|
||
setAudioUploadMessage('');
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
setLoading(true);
|
||
const payload = {
|
||
...values,
|
||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||
attendee_ids: values.attendee_ids || [],
|
||
tags: values.tags?.join(',') || '',
|
||
};
|
||
|
||
if (isEdit) {
|
||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meetingId)), payload);
|
||
message.success('会议更新成功');
|
||
} else {
|
||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||
if (res.code === '200') {
|
||
const newMeetingId = res.data.meeting_id;
|
||
if (selectedAudioFile) {
|
||
setAudioUploading(true);
|
||
setAudioUploadProgress(0);
|
||
setAudioUploadMessage('正在上传音频文件...');
|
||
try {
|
||
await uploadMeetingAudio({
|
||
meetingId: newMeetingId,
|
||
file: selectedAudioFile,
|
||
promptId: values.prompt_id,
|
||
onUploadProgress: (progressEvent) => {
|
||
if (progressEvent.total) {
|
||
setAudioUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
||
}
|
||
setAudioUploadMessage('正在上传音频文件...');
|
||
},
|
||
});
|
||
setAudioUploadProgress(100);
|
||
setAudioUploadMessage('上传完成,正在启动转录任务...');
|
||
message.success('会议创建成功,音频已开始上传处理');
|
||
} catch (uploadError) {
|
||
message.warning(uploadError?.response?.data?.message || uploadError?.response?.data?.detail || '会议已创建,但音频上传失败,请在详情页重试');
|
||
} finally {
|
||
setAudioUploading(false);
|
||
}
|
||
} else {
|
||
message.success('会议创建成功');
|
||
}
|
||
|
||
onSuccess?.(res.data.meeting_id);
|
||
onClose();
|
||
return;
|
||
}
|
||
}
|
||
onSuccess?.();
|
||
onClose();
|
||
} catch (error) {
|
||
if (!error?.errorFields) {
|
||
message.error(error?.response?.data?.message || error?.response?.data?.detail || '操作失败');
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Drawer
|
||
title={isEdit ? '编辑会议' : '新建会议'}
|
||
placement="right"
|
||
width={560}
|
||
open={open}
|
||
onClose={onClose}
|
||
destroyOnClose
|
||
extra={
|
||
<Space>
|
||
<Button type="primary" icon={<SaveOutlined />} loading={loading || audioUploading} onClick={handleSubmit}>
|
||
{isEdit ? '保存修改' : '创建会议'}
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
<Form form={form} layout="vertical" initialValues={{ meeting_time: dayjs() }}>
|
||
<Form.Item label="会议主题" name="title" rules={[{ required: true, message: '请输入会议主题' }]}>
|
||
<Input placeholder="请输入会议主题..." />
|
||
</Form.Item>
|
||
|
||
<Form.Item label="开始时间" name="meeting_time" rules={[{ required: true }]}>
|
||
<DatePicker showTime style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
|
||
<Form.Item label="使用总结模板" name="prompt_id">
|
||
<Select allowClear placeholder="选择总结模版">
|
||
{prompts.map((p) => (
|
||
<Select.Option key={p.id} value={p.id}>{p.name}</Select.Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
|
||
<Form.Item label="参会人员" name="attendee_ids" rules={[{ required: true, message: '请选择参会人员' }]}>
|
||
<Select mode="multiple" placeholder="选择参会人">
|
||
{users.map((u) => (
|
||
<Select.Option key={u.user_id} value={u.user_id}>{u.caption}</Select.Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
|
||
<Form.Item label="标签" name="tags">
|
||
<Select mode="tags" placeholder="输入标签按回车" />
|
||
</Form.Item>
|
||
|
||
{!isEdit ? (
|
||
<Form.Item label="会议音频">
|
||
<Card
|
||
variant="borderless"
|
||
style={{
|
||
borderRadius: 14,
|
||
border: '1px solid #d9e2f2',
|
||
background: selectedAudioFile ? 'linear-gradient(135deg, #f8fbff 0%, #ffffff 100%)' : '#fbfdff',
|
||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.86)',
|
||
}}
|
||
styles={{ body: { padding: 18 } }}
|
||
>
|
||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 16, alignItems: 'flex-start' }}>
|
||
<div>
|
||
<Text type="secondary">
|
||
支持 {AUDIO_UPLOAD_ACCEPT.replace(/\./g, '').toUpperCase()};
|
||
<br/>音频文件最大 {configService.formatFileSize(maxAudioSize)}。
|
||
</Text>
|
||
</div>
|
||
<Upload accept={AUDIO_UPLOAD_ACCEPT} showUploadList={false} beforeUpload={handleAudioBeforeUpload}>
|
||
<Button icon={<UploadOutlined />} disabled={audioUploading}>选择音频</Button>
|
||
</Upload>
|
||
</div>
|
||
|
||
{selectedAudioFile ? (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
gap: 16,
|
||
padding: '12px 14px',
|
||
borderRadius: 12,
|
||
background: '#ffffff',
|
||
border: '1px solid #dbe7f5',
|
||
}}
|
||
>
|
||
<Space size={12}>
|
||
<div
|
||
style={{
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: 12,
|
||
display: 'grid',
|
||
placeItems: 'center',
|
||
background: '#edf4ff',
|
||
color: '#1d4ed8',
|
||
}}
|
||
>
|
||
<AudioOutlined />
|
||
</div>
|
||
<div>
|
||
<Text strong style={{ display: 'block' }}>{selectedAudioFile.name}</Text>
|
||
<Text type="secondary">{configService.formatFileSize(selectedAudioFile.size)}</Text>
|
||
</div>
|
||
</Space>
|
||
<Button type="text" icon={<DeleteOutlined />} onClick={clearSelectedAudio} disabled={audioUploading}>
|
||
移除
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
borderRadius: 12,
|
||
border: '1px dashed #c7d5ea',
|
||
padding: '18px 16px',
|
||
background: '#ffffff',
|
||
color: 'rgba(17, 43, 78, 0.66)',
|
||
textAlign: 'center',
|
||
}}
|
||
>
|
||
可在会议详情中补传。
|
||
</div>
|
||
)}
|
||
|
||
{audioUploading ? (
|
||
<div
|
||
style={{
|
||
padding: '12px 14px',
|
||
borderRadius: 12,
|
||
background: '#f0f7ff',
|
||
border: '1px solid #bfdbfe',
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||
<Text strong>音频上传中</Text>
|
||
<Text strong style={{ color: '#1677ff' }}>{audioUploadProgress}%</Text>
|
||
</div>
|
||
<Text type="secondary" style={{ display: 'block', marginBottom: 10 }}>
|
||
{audioUploadMessage || '正在上传音频文件...'}
|
||
</Text>
|
||
<Progress percent={audioUploadProgress} status="active" strokeColor={{ from: '#69b1ff', to: '#1677ff' }} />
|
||
</div>
|
||
) : null}
|
||
</Space>
|
||
</Card>
|
||
</Form.Item>
|
||
) : null}
|
||
</Form>
|
||
</Drawer>
|
||
);
|
||
};
|
||
|
||
export default MeetingFormDrawer;
|