1.1.1
parent
af735bd93d
commit
f3d9429b28
|
|
@ -614,6 +614,13 @@ class SystemConfigService:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_branding_config(cls) -> Dict[str, str]:
|
def get_branding_config(cls) -> Dict[str, str]:
|
||||||
|
max_audio_size_mb = cls.get_max_audio_size(100)
|
||||||
|
max_image_size_mb = cls.get_config("max_image_size", "10")
|
||||||
|
try:
|
||||||
|
max_image_size_mb = int(max_image_size_mb)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
max_image_size_mb = 10
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"app_name": str(cls.get_config(cls.BRANDING_APP_NAME, "智听云平台") or "智听云平台"),
|
"app_name": str(cls.get_config(cls.BRANDING_APP_NAME, "智听云平台") or "智听云平台"),
|
||||||
"home_headline": str(cls.get_config(cls.BRANDING_HOME_HEADLINE, "智听云平台") or "智听云平台"),
|
"home_headline": str(cls.get_config(cls.BRANDING_HOME_HEADLINE, "智听云平台") or "智听云平台"),
|
||||||
|
|
@ -622,6 +629,10 @@ class SystemConfigService:
|
||||||
"preview_title": str(cls.get_config(cls.BRANDING_PREVIEW_TITLE, "会议预览") or "会议预览"),
|
"preview_title": str(cls.get_config(cls.BRANDING_PREVIEW_TITLE, "会议预览") or "会议预览"),
|
||||||
"login_welcome": str(cls.get_config(cls.BRANDING_LOGIN_WELCOME, "欢迎回来,请输入您的登录凭证。") or "欢迎回来,请输入您的登录凭证。"),
|
"login_welcome": str(cls.get_config(cls.BRANDING_LOGIN_WELCOME, "欢迎回来,请输入您的登录凭证。") or "欢迎回来,请输入您的登录凭证。"),
|
||||||
"footer_text": str(cls.get_config(cls.BRANDING_FOOTER_TEXT, "©2026 智听云平台") or "©2026 智听云平台"),
|
"footer_text": str(cls.get_config(cls.BRANDING_FOOTER_TEXT, "©2026 智听云平台") or "©2026 智听云平台"),
|
||||||
|
"max_audio_size": str(max_audio_size_mb),
|
||||||
|
"MAX_FILE_SIZE": max_audio_size_mb * 1024 * 1024,
|
||||||
|
"max_image_size": str(max_image_size_mb),
|
||||||
|
"MAX_IMAGE_SIZE": max_image_size_mb * 1024 * 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
# LLM模型配置获取方法(直接使用通用方法)
|
# LLM模型配置获取方法(直接使用通用方法)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const AudioPlayerBar = ({
|
||||||
moreMenuItems = [],
|
moreMenuItems = [],
|
||||||
emptyText = '暂无音频',
|
emptyText = '暂无音频',
|
||||||
showMoreButton = true,
|
showMoreButton = true,
|
||||||
|
moreButtonDisabled = false,
|
||||||
rateOptions = DEFAULT_RATE_OPTIONS,
|
rateOptions = DEFAULT_RATE_OPTIONS,
|
||||||
}) => {
|
}) => {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
|
@ -124,6 +125,18 @@ const AudioPlayerBar = ({
|
||||||
return (
|
return (
|
||||||
<div className="audio-player-bar is-empty">
|
<div className="audio-player-bar is-empty">
|
||||||
<span className="audio-player-bar-empty-text">{emptyText}</span>
|
<span className="audio-player-bar-empty-text">{emptyText}</span>
|
||||||
|
{showMoreButton ? (
|
||||||
|
<>
|
||||||
|
<span className="audio-player-bar-divider" />
|
||||||
|
{!moreButtonDisabled && moreMenuItems.length > 0 ? (
|
||||||
|
<Dropdown menu={{ items: moreMenuItems }} trigger={['click']}>
|
||||||
|
<Button type="text" className="audio-player-bar-control audio-player-bar-more" icon={<MoreOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
) : (
|
||||||
|
<Button type="text" disabled className="audio-player-bar-control audio-player-bar-more" icon={<MoreOutlined />} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +182,7 @@ const AudioPlayerBar = ({
|
||||||
{showMoreButton ? (
|
{showMoreButton ? (
|
||||||
<>
|
<>
|
||||||
<span className="audio-player-bar-divider" />
|
<span className="audio-player-bar-divider" />
|
||||||
{moreMenuItems.length > 0 ? (
|
{!moreButtonDisabled && moreMenuItems.length > 0 ? (
|
||||||
<Dropdown menu={{ items: moreMenuItems }} trigger={['click']}>
|
<Dropdown menu={{ items: moreMenuItems }} trigger={['click']}>
|
||||||
<Button type="text" className="audio-player-bar-control audio-player-bar-more" icon={<MoreOutlined />} />
|
<Button type="text" className="audio-player-bar-control audio-player-bar-more" icon={<MoreOutlined />} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App } from 'antd';
|
import { Drawer, Form, Input, Button, DatePicker, Select, Space, App, Upload, Card, Progress, Typography } from 'antd';
|
||||||
import { SaveOutlined } from '@ant-design/icons';
|
import { SaveOutlined, UploadOutlined, DeleteOutlined, AudioOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
import configService from '../utils/configService';
|
||||||
|
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
@ -14,17 +16,27 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [prompts, setPrompts] = 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 isEdit = Boolean(meetingId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
fetchOptions();
|
fetchOptions();
|
||||||
|
loadAudioUploadConfig();
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
fetchMeeting();
|
fetchMeeting();
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
form.setFieldsValue({ meeting_time: dayjs() });
|
form.setFieldsValue({ meeting_time: dayjs() });
|
||||||
|
setSelectedAudioFile(null);
|
||||||
|
setAudioUploading(false);
|
||||||
|
setAudioUploadProgress(0);
|
||||||
|
setAudioUploadMessage('');
|
||||||
}
|
}
|
||||||
}, [open, meetingId]);
|
}, [open, meetingId]);
|
||||||
|
|
||||||
|
|
@ -39,6 +51,15 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadAudioUploadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||||
|
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||||
|
} catch (error) {
|
||||||
|
setMaxAudioSize(100 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchMeeting = async () => {
|
const fetchMeeting = async () => {
|
||||||
setFetching(true);
|
setFetching(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -50,7 +71,6 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
||||||
prompt_id: meeting.prompt_id,
|
prompt_id: meeting.prompt_id,
|
||||||
tags: meeting.tags?.map((t) => t.name) || [],
|
tags: meeting.tags?.map((t) => t.name) || [],
|
||||||
description: meeting.description,
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载会议数据失败');
|
message.error('加载会议数据失败');
|
||||||
|
|
@ -59,6 +79,23 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
|
@ -76,7 +113,35 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
} else {
|
} else {
|
||||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||||
if (res.code === '200') {
|
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('会议创建成功');
|
message.success('会议创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
onSuccess?.(res.data.meeting_id);
|
onSuccess?.(res.data.meeting_id);
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
|
|
@ -103,7 +168,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" icon={<SaveOutlined />} loading={loading} onClick={handleSubmit}>
|
<Button type="primary" icon={<SaveOutlined />} loading={loading || audioUploading} onClick={handleSubmit}>
|
||||||
{isEdit ? '保存修改' : '创建会议'}
|
{isEdit ? '保存修改' : '创建会议'}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
@ -138,9 +203,105 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
<Select mode="tags" placeholder="输入标签按回车" />
|
<Select mode="tags" placeholder="输入标签按回车" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="会议备注" name="description">
|
{!isEdit ? (
|
||||||
<TextArea rows={3} placeholder="添加会议背景或说明..." />
|
<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>
|
</Form.Item>
|
||||||
|
) : null}
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Form, Input, Button, DatePicker, Select, Space,
|
Card, Form, Input, Button, DatePicker, Select,
|
||||||
Typography, App, Divider, Row, Col, Tag
|
Typography, App, Divider, Row, Col, Upload, Space, Progress
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined, UserOutlined, CalendarOutlined,
|
ArrowLeftOutlined, UploadOutlined, SaveOutlined, DeleteOutlined,
|
||||||
TeamOutlined, FileTextOutlined, PlusOutlined,
|
VideoCameraAddOutlined, AudioOutlined
|
||||||
UploadOutlined, SaveOutlined,
|
|
||||||
VideoCameraAddOutlined, TagOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
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 { Title, Text } = Typography;
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
const CreateMeeting = () => {
|
const CreateMeeting = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -24,10 +23,16 @@ const CreateMeeting = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [prompts, setPrompts] = 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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchPrompts();
|
fetchPrompts();
|
||||||
|
loadAudioUploadConfig();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
|
|
@ -44,18 +49,74 @@ const CreateMeeting = () => {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadAudioUploadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||||
|
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||||
|
} catch (error) {
|
||||||
|
setMaxAudioSize(100 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 onFinish = async (values) => {
|
const onFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
...values,
|
...values,
|
||||||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
attendee_ids: values.attendee_ids
|
attendee_ids: values.attendee_ids,
|
||||||
|
tags: values.tags?.join(',') || ''
|
||||||
};
|
};
|
||||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||||
if (res.code === '200') {
|
if (res.code === '200') {
|
||||||
|
const meetingId = res.data.meeting_id;
|
||||||
|
|
||||||
|
if (selectedAudioFile) {
|
||||||
|
setAudioUploading(true);
|
||||||
|
setAudioUploadProgress(0);
|
||||||
|
setAudioUploadMessage('正在上传音频文件...');
|
||||||
|
try {
|
||||||
|
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('会议创建成功,音频已开始上传处理');
|
||||||
|
} catch (uploadError) {
|
||||||
|
message.warning(uploadError.response?.data?.message || uploadError.response?.data?.detail || '会议已创建,但音频上传失败,请在详情页重试');
|
||||||
|
} finally {
|
||||||
|
setAudioUploading(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
message.success('会议创建成功');
|
message.success('会议创建成功');
|
||||||
navigate(`/meetings/${res.data.meeting_id}`);
|
}
|
||||||
|
|
||||||
|
navigate(`/meetings/${meetingId}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.response?.data?.message || error.response?.data?.detail || '创建失败');
|
message.error(error.response?.data?.message || error.response?.data?.detail || '创建失败');
|
||||||
|
|
@ -102,15 +163,115 @@ const CreateMeeting = () => {
|
||||||
<Select mode="tags" size="large" placeholder="输入标签按回车" />
|
<Select mode="tags" size="large" placeholder="输入标签按回车" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="会议备注" name="description">
|
<Form.Item label="会议音频">
|
||||||
<TextArea rows={4} placeholder="添加会议背景或说明..." />
|
<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()},
|
||||||
|
最大 {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>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" size="large" icon={<SaveOutlined />} loading={loading} block style={{ height: 50, fontSize: 16, borderRadius: 8 }}>
|
<Button type="primary" htmlType="submit" size="large" icon={<SaveOutlined />} loading={loading || audioUploading} block style={{ height: 50, fontSize: 16, borderRadius: 8 }}>
|
||||||
创建并进入详情
|
{selectedAudioFile ? '创建并上传音频' : '创建并进入详情'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Form, Input, Button, DatePicker, Select, Space,
|
Card, Form, Input, Button, DatePicker, Select,
|
||||||
Typography, App, Divider, Row, Col
|
Typography, App, Divider, Row, Col
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined, TeamOutlined, SaveOutlined,
|
ArrowLeftOutlined, SaveOutlined, VideoCameraAddOutlined
|
||||||
VideoCameraAddOutlined, TagOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
@ -13,7 +12,6 @@ import apiClient from '../utils/apiClient';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
const { TextArea } = Input;
|
|
||||||
|
|
||||||
const EditMeeting = () => {
|
const EditMeeting = () => {
|
||||||
const { meeting_id } = useParams();
|
const { meeting_id } = useParams();
|
||||||
|
|
@ -111,10 +109,6 @@ const EditMeeting = () => {
|
||||||
<Select mode="tags" size="large" placeholder="输入标签按回车" />
|
<Select mode="tags" size="large" placeholder="输入标签按回车" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="会议备注" name="description">
|
|
||||||
<TextArea rows={4} placeholder="添加会议背景或说明..." />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import tools from '../utils/tools';
|
||||||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
import QRCodeModal from '../components/QRCodeModal';
|
import QRCodeModal from '../components/QRCodeModal';
|
||||||
import MeetingFormDrawer from '../components/MeetingFormDrawer';
|
import MeetingFormDrawer from '../components/MeetingFormDrawer';
|
||||||
|
import configService from '../utils/configService';
|
||||||
|
import { AUDIO_UPLOAD_ACCEPT, uploadMeetingAudio, validateMeetingAudioFile } from '../services/meetingAudioService';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
@ -115,6 +117,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false);
|
const [accessPasswordEnabled, setAccessPasswordEnabled] = useState(false);
|
||||||
const [accessPasswordDraft, setAccessPasswordDraft] = useState('');
|
const [accessPasswordDraft, setAccessPasswordDraft] = useState('');
|
||||||
const [savingAccessPassword, setSavingAccessPassword] = useState(false);
|
const [savingAccessPassword, setSavingAccessPassword] = useState(false);
|
||||||
|
const [maxAudioSize, setMaxAudioSize] = useState(100 * 1024 * 1024);
|
||||||
|
|
||||||
// 转录编辑 Drawer
|
// 转录编辑 Drawer
|
||||||
const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false);
|
const [showTranscriptEditDrawer, setShowTranscriptEditDrawer] = useState(false);
|
||||||
|
|
@ -159,6 +162,7 @@ const MeetingDetails = ({ user }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMeetingDetails();
|
fetchMeetingDetails();
|
||||||
fetchTranscript();
|
fetchTranscript();
|
||||||
|
loadAudioUploadConfig();
|
||||||
return () => {
|
return () => {
|
||||||
if (statusCheckIntervalRef.current) clearInterval(statusCheckIntervalRef.current);
|
if (statusCheckIntervalRef.current) clearInterval(statusCheckIntervalRef.current);
|
||||||
if (summaryPollIntervalRef.current) clearInterval(summaryPollIntervalRef.current);
|
if (summaryPollIntervalRef.current) clearInterval(summaryPollIntervalRef.current);
|
||||||
|
|
@ -166,6 +170,15 @@ const MeetingDetails = ({ user }) => {
|
||||||
};
|
};
|
||||||
}, [meeting_id]);
|
}, [meeting_id]);
|
||||||
|
|
||||||
|
const loadAudioUploadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const nextMaxAudioSize = await configService.getMaxAudioSize();
|
||||||
|
setMaxAudioSize(nextMaxAudioSize || 100 * 1024 * 1024);
|
||||||
|
} catch (error) {
|
||||||
|
setMaxAudioSize(100 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSummaryDrawer) {
|
if (!showSummaryDrawer) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -490,27 +503,27 @@ const MeetingDetails = ({ user }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUploadAudio = async (file) => {
|
const handleUploadAudio = async (file) => {
|
||||||
const formData = new FormData();
|
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||||
formData.append('audio_file', file);
|
if (validationMessage) {
|
||||||
formData.append('meeting_id', meeting_id);
|
message.warning(validationMessage);
|
||||||
formData.append('force_replace', 'true');
|
throw new Error(validationMessage);
|
||||||
if (meeting?.prompt_id) {
|
|
||||||
formData.append('prompt_id', String(meeting.prompt_id));
|
|
||||||
}
|
|
||||||
if (selectedModelCode) {
|
|
||||||
formData.append('model_code', selectedModelCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
setUploadStatusMessage('正在上传音频文件...');
|
setUploadStatusMessage('正在上传音频文件...');
|
||||||
try {
|
try {
|
||||||
await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData, {
|
await uploadMeetingAudio({
|
||||||
|
meetingId: meeting_id,
|
||||||
|
file,
|
||||||
|
promptId: meeting?.prompt_id,
|
||||||
|
modelCode: selectedModelCode,
|
||||||
onUploadProgress: (progressEvent) => {
|
onUploadProgress: (progressEvent) => {
|
||||||
if (progressEvent.total) {
|
if (progressEvent.total) {
|
||||||
setUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
setUploadProgress(Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)));
|
||||||
}
|
}
|
||||||
setUploadStatusMessage('正在上传音频文件...');
|
setUploadStatusMessage('正在上传音频文件...');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
setUploadStatusMessage('上传完成,正在启动转录任务...');
|
setUploadStatusMessage('上传完成,正在启动转录任务...');
|
||||||
|
|
@ -944,6 +957,15 @@ const MeetingDetails = ({ user }) => {
|
||||||
<Upload
|
<Upload
|
||||||
id="audio-upload-input"
|
id="audio-upload-input"
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
|
accept={AUDIO_UPLOAD_ACCEPT}
|
||||||
|
beforeUpload={(file) => {
|
||||||
|
const validationMessage = validateMeetingAudioFile(file, maxAudioSize, configService.formatFileSize.bind(configService));
|
||||||
|
if (validationMessage) {
|
||||||
|
message.warning(validationMessage);
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
customRequest={async ({ file, onSuccess, onError }) => {
|
customRequest={async ({ file, onSuccess, onError }) => {
|
||||||
try {
|
try {
|
||||||
await handleUploadAudio(file);
|
await handleUploadAudio(file);
|
||||||
|
|
@ -989,7 +1011,8 @@ const MeetingDetails = ({ user }) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
moreMenuItems={audioMoreMenuItems}
|
moreMenuItems={audioMoreMenuItems}
|
||||||
emptyText="暂无音频,可通过右侧更多操作上传音频"
|
moreButtonDisabled={!isMeetingOwner}
|
||||||
|
emptyText="暂无音频,可通过'更多'上传音频"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import apiClient from '../utils/apiClient';
|
||||||
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
|
||||||
|
export const AUDIO_UPLOAD_ACCEPT = '.mp3,.wav,.m4a,.mpeg,.mp4';
|
||||||
|
const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.m4a', '.mpeg', '.mp4'];
|
||||||
|
|
||||||
|
export const validateMeetingAudioFile = (file, maxAudioSize, formatFileSize) => {
|
||||||
|
if (!file) {
|
||||||
|
return '请选择音频文件';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = String(file.name || '').toLowerCase();
|
||||||
|
const isAllowed = AUDIO_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
||||||
|
if (!isAllowed) {
|
||||||
|
return `仅支持 ${AUDIO_EXTENSIONS.join(' / ').toUpperCase().replace(/\./g, '')} 格式音频`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxAudioSize && file.size > maxAudioSize) {
|
||||||
|
const readableSize = formatFileSize ? formatFileSize(maxAudioSize) : `${Math.round(maxAudioSize / 1024 / 1024)} MB`;
|
||||||
|
return `音频大小不能超过 ${readableSize}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadMeetingAudio = async ({
|
||||||
|
meetingId,
|
||||||
|
file,
|
||||||
|
promptId,
|
||||||
|
modelCode,
|
||||||
|
onUploadProgress,
|
||||||
|
}) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio_file', file);
|
||||||
|
formData.append('meeting_id', meetingId);
|
||||||
|
|
||||||
|
if (promptId) {
|
||||||
|
formData.append('prompt_id', String(promptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelCode) {
|
||||||
|
formData.append('model_code', modelCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.UPLOAD_AUDIO), formData, {
|
||||||
|
onUploadProgress,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -36,7 +36,7 @@ class ConfigService {
|
||||||
|
|
||||||
async loadConfigsFromServer() {
|
async loadConfigsFromServer() {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.ADMIN.SYSTEM_CONFIG));
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PUBLIC.SYSTEM_CONFIG));
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load system configs, using defaults:', error);
|
console.warn('Failed to load system configs, using defaults:', error);
|
||||||
|
|
@ -53,6 +53,10 @@ class ConfigService {
|
||||||
return configs.MAX_FILE_SIZE || 100 * 1024 * 1024;
|
return configs.MAX_FILE_SIZE || 100 * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMaxAudioSize() {
|
||||||
|
return this.getMaxFileSize();
|
||||||
|
}
|
||||||
|
|
||||||
async getMaxImageSize() {
|
async getMaxImageSize() {
|
||||||
const configs = await this.getConfigs();
|
const configs = await this.getConfigs();
|
||||||
return configs.MAX_IMAGE_SIZE || 10 * 1024 * 1024;
|
return configs.MAX_IMAGE_SIZE || 10 * 1024 * 1024;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue