imetting/frontend/src/pages/CreateMeeting.jsx

293 lines
12 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, { useCallback, useEffect, useState } from 'react';
import {
Card, Form, Input, Button, DatePicker, Select,
Typography, App, Divider, Row, Col, Upload, Space, Progress
} from 'antd';
import {
ArrowLeftOutlined, UploadOutlined, SaveOutlined, DeleteOutlined,
VideoCameraAddOutlined, AudioOutlined
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import httpService from '../services/httpService';
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
import configService from '../utils/configService';
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
const { Title, Text } = Typography;
const CreateMeeting = () => {
const navigate = useNavigate();
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(() => configService.getCachedMaxAudioSize());
const fetchUsers = useCallback(async () => {
try {
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
setUsers(res.data.users || []);
} catch {
setUsers([]);
}
}, []);
const fetchPrompts = useCallback(async () => {
try {
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
setPrompts(res.data.prompts || []);
} catch {
setPrompts([]);
}
}, []);
const loadAudioUploadConfig = useCallback(async () => {
try {
const nextMaxAudioSize = await configService.getMaxAudioSize();
setMaxAudioSize(nextMaxAudioSize);
} catch (error) {
message.error(error?.message || '加载音频上传限制失败');
}
}, [message]);
useEffect(() => {
fetchUsers();
fetchPrompts();
loadAudioUploadConfig();
}, [fetchPrompts, fetchUsers, loadAudioUploadConfig]);
const handleAudioBeforeUpload = (file) => {
if (!maxAudioSize) {
message.error('系统音频上传限制未加载完成,请稍后重试');
return Upload.LIST_IGNORE;
}
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 onFinish = async (values) => {
setLoading(true);
try {
const payload = {
...values,
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
attendee_ids: values.attendee_ids || [],
tags: values.tags?.join(',') || ''
};
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
if (res.code === '200') {
const meetingId = res.data.meeting_id;
if (selectedAudioFile) {
setAudioUploading(true);
setAudioUploadProgress(0);
setAudioUploadMessage('正在上传音频文件...');
try {
const uploadResponse = await uploadMeetingAudio({
meetingId,
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(uploadResponse?.message || '会议创建成功,音频已进入后台处理');
} catch (uploadError) {
message.warning(uploadError.response?.data?.message || uploadError.response?.data?.detail || '会议已创建,但音频上传失败,请在详情页重试');
} finally {
setAudioUploading(false);
}
} else {
message.success('会议创建成功');
}
navigate(`/meetings/${meetingId}`);
}
} catch (error) {
message.error(error.response?.data?.message || error.response?.data?.detail || '创建失败');
} finally {
setLoading(false);
}
};
return (
<div className="create-meeting-modern" style={{ maxWidth: 800, margin: '0 auto', padding: '24px 0' }}>
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 32 }}>
<Button icon={<ArrowLeftOutlined />} shape="circle" onClick={() => navigate(-1)} style={{ marginRight: 16 }} />
<Title level={3} style={{ margin: 0 }}>新建会议纪要</Title>
</div>
<Form form={form} layout="vertical" onFinish={onFinish} initialValues={{ meeting_time: dayjs() }}>
<Form.Item label="会议主题" name="title" rules={[{ required: true, message: '请输入会议主题' }]}>
<Input size="large" prefix={<VideoCameraAddOutlined />} placeholder="请输入会议主题..." />
</Form.Item>
<Row gutter={24}>
<Col span={12}>
<Form.Item label="开始时间" name="meeting_time" rules={[{ required: true }]}>
<DatePicker showTime size="large" style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="使用总结模板" name="prompt_id">
<Select size="large" placeholder="选择总结模版">
{prompts.map(p => <Select.Option key={p.id} value={p.id}>{p.name}</Select.Option>)}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item label="参会人员" name="attendee_ids">
<Select mode="multiple" size="large" 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" size="large" placeholder="输入标签按回车" />
</Form.Item>
<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 strong style={{ display: 'block', marginBottom: 4 }}>上传会议音频</Text>
<Text type="secondary">
创建后会自动沿用会议详情页同一套上传与转录逻辑支持 {AUDIO_UPLOAD_ACCEPT.replace(/\./g, '').toUpperCase()}
最大 {maxAudioSize ? 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>
<Divider />
<Form.Item>
<Button type="primary" htmlType="submit" size="large" icon={<SaveOutlined />} loading={loading || audioUploading} block style={{ height: 50, fontSize: 16, borderRadius: 8 }}>
{selectedAudioFile ? '创建并上传音频' : '创建并进入详情'}
</Button>
</Form.Item>
</Form>
</Card>
</div>
);
};
export default CreateMeeting;