feat: 增加音频上传超时和进度显示
为音频文件上传接口增加超时设置,防止大文件上传时请求超时。在 speaker 注册接口设置 2 分钟超时,在会议音频上传接口设置 5 分钟超时。同时为上传音频功能添加真实的进度回调,替换模拟进度,提升用户体验。 在首页点击创建会议按钮时,改为直接打开创建抽屉,而不是跳转到会议页面再打开,简化用户操作流程。dev_na
parent
53ff2292a8
commit
5f895bfe26
|
|
@ -297,13 +297,17 @@ export const updateMeetingParticipants = (params: UpdateMeetingParticipantsComma
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadAudio = (file: File) => {
|
export const uploadAudio = (file: File, onUploadProgress?: (progressEvent: any) => void) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
return http.post<{ code: string; data: string; msg: string }>(
|
return http.post<{ code: string; data: string; msg: string }>(
|
||||||
"/api/biz/meeting/upload",
|
"/api/biz/meeting/upload",
|
||||||
formData,
|
formData,
|
||||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
{
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
timeout: 300000, // 5 minutes timeout for large audio files
|
||||||
|
onUploadProgress
|
||||||
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ export const registerSpeaker = (params: SpeakerRegisterParams) => {
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
},
|
||||||
|
timeout: 120000 // 2 minutes timeout for audio files
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -149,13 +149,20 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({ open,
|
||||||
}, [type, form, open]);
|
}, [type, form, open]);
|
||||||
|
|
||||||
const customUpload = async (options: any) => {
|
const customUpload = async (options: any) => {
|
||||||
const { file, onSuccess: uploadSuccess, onError } = options;
|
const { file, onSuccess: uploadSuccess, onError, onProgress } = options;
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
try {
|
try {
|
||||||
const interval = setInterval(() => setUploadProgress(prev => (prev < 95 ? prev + 5 : prev)), 300);
|
const res = await uploadAudio(file, (progressEvent) => {
|
||||||
const res = await uploadAudio(file);
|
if (progressEvent.total) {
|
||||||
clearInterval(interval);
|
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
|
// Only show up to 99% during upload, save 100% for actual completion
|
||||||
|
const displayPercent = percentCompleted > 99 ? 99 : percentCompleted;
|
||||||
|
setUploadProgress(displayPercent);
|
||||||
|
onProgress({ percent: displayPercent });
|
||||||
|
}
|
||||||
|
});
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
|
onProgress({ percent: 100 });
|
||||||
setAudioUrl(res.data.data);
|
setAudioUrl(res.data.data);
|
||||||
uploadSuccess(res.data.data);
|
uploadSuccess(res.data.data);
|
||||||
message.success('录音上传成功');
|
message.success('录音上传成功');
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { usePermission } from '../../hooks/usePermission';
|
import { usePermission } from '../../hooks/usePermission';
|
||||||
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, uploadAudio, updateMeetingParticipants, getRealtimeMeetingSessionStatus, getRealtimeMeetingSessionStatuses, RealtimeMeetingSessionStatus } from '../../api/business/meeting';
|
import { getMeetingPage, deleteMeeting, MeetingVO, getMeetingProgress, MeetingProgress, createMeeting, updateMeetingParticipants, getRealtimeMeetingSessionStatus, getRealtimeMeetingSessionStatuses, RealtimeMeetingSessionStatus } from '../../api/business/meeting';
|
||||||
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
|
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
|
||||||
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
|
||||||
import { getHotWordPage, HotWordVO } from '../../api/business/hotword';
|
import { getHotWordPage, HotWordVO } from '../../api/business/hotword';
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { getRecentTasks } from "@/api/business/dashboard";
|
||||||
import type { MeetingVO } from "@/api/business/meeting";
|
import type { MeetingVO } from "@/api/business/meeting";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
import RightVisual from "./RightVisual";
|
import RightVisual from "./RightVisual";
|
||||||
|
import { MeetingCreateDrawer, MeetingCreateType } from "@/components/business/MeetingCreateDrawer";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
|
@ -75,6 +76,8 @@ export default function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
|
const [recentTasks, setRecentTasks] = useState<MeetingVO[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [drawerType, setDrawerType] = useState<MeetingCreateType>('realtime');
|
||||||
const [readCardIds, setReadCardIds] = useState<string[]>(() => {
|
const [readCardIds, setReadCardIds] = useState<string[]>(() => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -134,7 +137,10 @@ export default function HomePage() {
|
||||||
description: ["实时语音转文字", "同步翻译,智能总结要点"],
|
description: ["实时语音转文字", "同步翻译,智能总结要点"],
|
||||||
accent: "violet",
|
accent: "violet",
|
||||||
badge: "会议神器",
|
badge: "会议神器",
|
||||||
onClick: () => navigate("/meetings?action=create&type=realtime")
|
onClick: () => {
|
||||||
|
setDrawerType("realtime");
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "上传音视频",
|
title: "上传音视频",
|
||||||
|
|
@ -142,10 +148,13 @@ export default function HomePage() {
|
||||||
description: ["音视频转文字", "区分发言人,一键导出"],
|
description: ["音视频转文字", "区分发言人,一键导出"],
|
||||||
accent: "cyan",
|
accent: "cyan",
|
||||||
badge: "iMeeting",
|
badge: "iMeeting",
|
||||||
onClick: () => navigate("/meetings?action=create&type=upload")
|
onClick: () => {
|
||||||
|
setDrawerType("upload");
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[navigate]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
|
const recentCards = useMemo(() => buildRecentCards(recentTasks), [recentTasks]);
|
||||||
|
|
@ -271,6 +280,18 @@ export default function HomePage() {
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MeetingCreateDrawer
|
||||||
|
open={drawerOpen}
|
||||||
|
initialType={drawerType}
|
||||||
|
onCancel={() => setDrawerOpen(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
// onSuccess doesn't return meeting id in MeetingCreateDrawerProps right now
|
||||||
|
// the Drawer handles its own navigation or we can just redirect to meetings list
|
||||||
|
navigate('/meetings');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue