2026-04-08 09:29:06 +00:00
|
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import {
|
2026-04-07 10:48:35 +00:00
|
|
|
|
Card, Form, Input, Button, DatePicker, Select,
|
|
|
|
|
|
Typography, App, Divider, Row, Col, Upload, Space, Progress
|
2026-03-26 06:55:12 +00:00
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
2026-04-07 10:48:35 +00:00
|
|
|
|
ArrowLeftOutlined, UploadOutlined, SaveOutlined, DeleteOutlined,
|
|
|
|
|
|
VideoCameraAddOutlined, AudioOutlined
|
2026-03-26 06:55:12 +00:00
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2026-04-08 11:19:33 +00:00
|
|
|
|
import httpService from '../services/httpService';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
2026-04-07 10:48:35 +00:00
|
|
|
|
import configService from '../utils/configService';
|
|
|
|
|
|
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-27 07:43:08 +00:00
|
|
|
|
const CreateMeeting = () => {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const navigate = useNavigate();
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const { message } = App.useApp();
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [users, setUsers] = useState([]);
|
|
|
|
|
|
const [prompts, setPrompts] = useState([]);
|
2026-04-07 10:48:35 +00:00
|
|
|
|
const [selectedAudioFile, setSelectedAudioFile] = useState(null);
|
|
|
|
|
|
const [audioUploading, setAudioUploading] = useState(false);
|
|
|
|
|
|
const [audioUploadProgress, setAudioUploadProgress] = useState(0);
|
|
|
|
|
|
const [audioUploadMessage, setAudioUploadMessage] = useState('');
|
2026-04-14 01:56:57 +00:00
|
|
|
|
const [maxAudioSize, setMaxAudioSize] = useState(() => configService.getCachedMaxAudioSize());
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchUsers = useCallback(async () => {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.USERS.LIST));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setUsers(res.data.users || []);
|
2026-04-08 09:29:06 +00:00
|
|
|
|
} catch {
|
|
|
|
|
|
setUsers([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchPrompts = useCallback(async () => {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('MEETING_TASK')));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setPrompts(res.data.prompts || []);
|
2026-04-08 09:29:06 +00:00
|
|
|
|
} catch {
|
|
|
|
|
|
setPrompts([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const loadAudioUploadConfig = useCallback(async () => {
|
2026-04-07 10:48:35 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
2026-04-14 01:56:57 +00:00
|
|
|
|
setMaxAudioSize(nextMaxAudioSize);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(error?.message || '加载音频上传限制失败');
|
2026-04-07 10:48:35 +00:00
|
|
|
|
}
|
2026-04-14 01:56:57 +00:00
|
|
|
|
}, [message]);
|
2026-04-08 09:29:06 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
fetchPrompts();
|
|
|
|
|
|
loadAudioUploadConfig();
|
|
|
|
|
|
}, [fetchPrompts, fetchUsers, loadAudioUploadConfig]);
|
2026-04-07 10:48:35 +00:00
|
|
|
|
|
|
|
|
|
|
const handleAudioBeforeUpload = (file) => {
|
2026-04-14 01:56:57 +00:00
|
|
|
|
if (!maxAudioSize) {
|
|
|
|
|
|
message.error('系统音频上传限制未加载完成,请稍后重试');
|
|
|
|
|
|
return Upload.LIST_IGNORE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 10:48:35 +00:00
|
|
|
|
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('');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const onFinish = async (values) => {
|
|
|
|
|
|
setLoading(true);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const payload = {
|
|
|
|
|
|
...values,
|
|
|
|
|
|
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
2026-04-15 06:25:46 +00:00
|
|
|
|
attendee_ids: values.attendee_ids || [],
|
2026-04-07 10:48:35 +00:00
|
|
|
|
tags: values.tags?.join(',') || ''
|
2026-01-19 11:03:08 +00:00
|
|
|
|
};
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const res = await httpService.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (res.code === '200') {
|
2026-04-07 10:48:35 +00:00
|
|
|
|
const meetingId = res.data.meeting_id;
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedAudioFile) {
|
|
|
|
|
|
setAudioUploading(true);
|
|
|
|
|
|
setAudioUploadProgress(0);
|
|
|
|
|
|
setAudioUploadMessage('正在上传音频文件...');
|
|
|
|
|
|
try {
|
2026-04-09 11:43:00 +00:00
|
|
|
|
const uploadResponse = await uploadMeetingAudio({
|
2026-04-07 10:48:35 +00:00
|
|
|
|
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);
|
2026-04-09 11:43:00 +00:00
|
|
|
|
setAudioUploadMessage('上传完成,后台正在处理音频...');
|
|
|
|
|
|
message.success(uploadResponse?.message || '会议创建成功,音频已进入后台处理');
|
2026-04-07 10:48:35 +00:00
|
|
|
|
} catch (uploadError) {
|
|
|
|
|
|
message.warning(uploadError.response?.data?.message || uploadError.response?.data?.detail || '会议已创建,但音频上传失败,请在详情页重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setAudioUploading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.success('会议创建成功');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
navigate(`/meetings/${meetingId}`);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
} catch (error) {
|
2026-03-27 07:43:08 +00:00
|
|
|
|
message.error(error.response?.data?.message || error.response?.data?.detail || '创建失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} finally {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setLoading(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div className="create-meeting-modern" style={{ maxWidth: 800, margin: '0 auto', padding: '24px 0' }}>
|
2026-03-26 11:51:00 +00:00
|
|
|
|
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: '0 4px 20px rgba(0,0,0,0.05)' }}>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<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>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-04-15 06:25:46 +00:00
|
|
|
|
<Form.Item label="参会人员" name="attendee_ids">
|
|
|
|
|
|
<Select mode="multiple" size="large" placeholder="可不选参会人">
|
2026-03-27 07:43:08 +00:00
|
|
|
|
{users.map(u => <Select.Option key={u.user_id} value={u.user_id}>{u.caption}</Select.Option>)}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="标签" name="tags">
|
|
|
|
|
|
<Select mode="tags" size="large" placeholder="输入标签按回车" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-04-07 10:48:35 +00:00
|
|
|
|
<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()},
|
2026-04-14 01:56:57 +00:00
|
|
|
|
最大 {maxAudioSize ? configService.formatFileSize(maxAudioSize) : '加载中'}。
|
2026-04-07 10:48:35 +00:00
|
|
|
|
</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>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item>
|
2026-04-07 10:48:35 +00:00
|
|
|
|
<Button type="primary" htmlType="submit" size="large" icon={<SaveOutlined />} loading={loading || audioUploading} block style={{ height: 50, fontSize: 16, borderRadius: 8 }}>
|
|
|
|
|
|
{selectedAudioFile ? '创建并上传音频' : '创建并进入详情'}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Card>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
export default CreateMeeting;
|