2026-05-13 06:31:48 +00:00
|
|
|
|
import React, { useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
import {
|
|
|
|
|
|
App,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Collapse,
|
|
|
|
|
|
DatePicker,
|
|
|
|
|
|
Drawer,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Progress,
|
|
|
|
|
|
Radio,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
Skeleton,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Switch,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
} from "antd";
|
|
|
|
|
|
import {
|
|
|
|
|
|
AudioOutlined,
|
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
|
CloudUploadOutlined,
|
|
|
|
|
|
QuestionCircleOutlined,
|
|
|
|
|
|
SettingOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
|
|
|
|
|
|
|
|
import { listUsers } from "../../api";
|
|
|
|
|
|
import { getAiModelDefault, getAiModelPage, type AiModelVO } from "../../api/business/aimodel";
|
|
|
|
|
|
import { getHotWordPage, type HotWordVO } from "../../api/business/hotword";
|
|
|
|
|
|
import { getHotWordGroupOptions, type HotWordGroupVO } from "../../api/business/hotwordGroup";
|
|
|
|
|
|
import {
|
|
|
|
|
|
createMeeting,
|
|
|
|
|
|
createRealtimeMeeting,
|
|
|
|
|
|
getMeetingCreateConfig,
|
|
|
|
|
|
type CreateRealtimeMeetingCommand,
|
|
|
|
|
|
type MeetingCreateConfig,
|
|
|
|
|
|
type SummaryDetailLevel,
|
|
|
|
|
|
uploadAudio,
|
|
|
|
|
|
} from "../../api/business/meeting";
|
|
|
|
|
|
import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt";
|
|
|
|
|
|
import type { SysUser } from "../../types";
|
2026-06-30 05:37:33 +00:00
|
|
|
|
import "./MeetingCreateDrawer.css";
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
|
|
|
|
|
const { Dragger } = Upload;
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const { Option } = Select;
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
export type MeetingCreateType = "upload" | "realtime";
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
2026-04-24 07:47:52 +00:00
|
|
|
|
const DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB = 1024;
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = {
|
|
|
|
|
|
offlineEnabled: true,
|
|
|
|
|
|
realtimeEnabled: true,
|
|
|
|
|
|
offlineAudioMaxSizeMb: DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB,
|
|
|
|
|
|
};
|
2026-04-24 07:47:52 +00:00
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
interface MeetingCreateDrawerProps {
|
|
|
|
|
|
open: boolean;
|
|
|
|
|
|
initialType?: MeetingCreateType;
|
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
|
onSuccess: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type RealtimeMeetingSessionDraft = {
|
|
|
|
|
|
meetingId: number;
|
|
|
|
|
|
meetingTitle: string;
|
|
|
|
|
|
asrModelName: string;
|
|
|
|
|
|
summaryModelName: string;
|
|
|
|
|
|
asrModelId: number;
|
|
|
|
|
|
mode: string;
|
|
|
|
|
|
language: string;
|
|
|
|
|
|
useSpkId: number;
|
|
|
|
|
|
enablePunctuation: boolean;
|
|
|
|
|
|
enableItn: boolean;
|
|
|
|
|
|
enableTextRefine: boolean;
|
|
|
|
|
|
saveAudio: boolean;
|
|
|
|
|
|
hotwords: Array<{ hotword: string; weight: number }>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
function resolveAvailableCreateTypes(config: MeetingCreateConfig): MeetingCreateType[] {
|
|
|
|
|
|
const types: MeetingCreateType[] = [];
|
|
|
|
|
|
if (config.offlineEnabled) {
|
|
|
|
|
|
types.push("upload");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (config.realtimeEnabled) {
|
|
|
|
|
|
types.push("realtime");
|
|
|
|
|
|
}
|
|
|
|
|
|
return types;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveAvailableCreateType(initialType: MeetingCreateType, config: MeetingCreateConfig): MeetingCreateType {
|
|
|
|
|
|
const availableTypes = resolveAvailableCreateTypes(config);
|
|
|
|
|
|
if (availableTypes.includes(initialType)) {
|
|
|
|
|
|
return initialType;
|
|
|
|
|
|
}
|
|
|
|
|
|
return availableTypes[0] || initialType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
function resolveWsUrl(model?: AiModelVO | null) {
|
|
|
|
|
|
if (model?.wsUrl) return model.wsUrl;
|
|
|
|
|
|
if (model?.baseUrl) return model.baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getSessionKey(meetingId: number) {
|
|
|
|
|
|
return `realtimeMeetingSession:${meetingId}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
|
|
|
|
|
|
open,
|
|
|
|
|
|
initialType = "upload",
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
onSuccess,
|
|
|
|
|
|
}) => {
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const { message } = App.useApp();
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [form] = Form.useForm();
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const [type, setType] = useState<MeetingCreateType>(initialType);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const [configLoaded, setConfigLoaded] = useState(false);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const [createConfig, setCreateConfig] = useState<MeetingCreateConfig>({
|
|
|
|
|
|
offlineEnabled: false,
|
|
|
|
|
|
realtimeEnabled: false,
|
|
|
|
|
|
offlineAudioMaxSizeMb: DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const [asrModels, setAsrModels] = useState<AiModelVO[]>([]);
|
|
|
|
|
|
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
|
|
|
|
|
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
|
|
|
|
|
const [hotwordList, setHotwordList] = useState<HotWordVO[]>([]);
|
2026-04-27 02:39:34 +00:00
|
|
|
|
const [hotWordGroups, setHotWordGroups] = useState<HotWordGroupVO[]>([]);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const [userList, setUserList] = useState<SysUser[]>([]);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
|
|
|
|
|
const [audioUrl, setAudioUrl] = useState("");
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
|
|
|
|
const [fileList, setFileList] = useState<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
const watchedAsrModelId = Form.useWatch("asrModelId", form);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const watchedSummaryModelId = Form.useWatch("summaryModelId", form);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const watchedPromptId = Form.useWatch("promptId", form);
|
2026-04-27 02:39:34 +00:00
|
|
|
|
const watchedHotWordGroupId = Form.useWatch("hotWordGroupId", form);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const selectedAsrModel = useMemo(
|
|
|
|
|
|
() => asrModels.find((item) => item.id === watchedAsrModelId) || null,
|
|
|
|
|
|
[asrModels, watchedAsrModelId]
|
|
|
|
|
|
);
|
|
|
|
|
|
const selectedSummaryModel = useMemo(
|
|
|
|
|
|
() => llmModels.find((item) => item.id === watchedSummaryModelId) || null,
|
|
|
|
|
|
[llmModels, watchedSummaryModelId]
|
|
|
|
|
|
);
|
|
|
|
|
|
const selectedPrompt = useMemo(
|
|
|
|
|
|
() => prompts.find((item) => item.id === watchedPromptId) || null,
|
|
|
|
|
|
[prompts, watchedPromptId]
|
|
|
|
|
|
);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const availableTypes = useMemo(() => resolveAvailableCreateTypes(createConfig), [createConfig]);
|
|
|
|
|
|
const offlineAudioMaxSizeBytes = useMemo(
|
|
|
|
|
|
() => (createConfig.offlineAudioMaxSizeMb || DEFAULT_OFFLINE_AUDIO_MAX_SIZE_MB) * 1024 * 1024,
|
|
|
|
|
|
[createConfig]
|
|
|
|
|
|
);
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
2026-04-27 02:39:34 +00:00
|
|
|
|
useEffect(() => {
|
2026-05-13 06:31:48 +00:00
|
|
|
|
if (!open) {
|
2026-04-27 02:39:34 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
setAudioUrl("");
|
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
|
setFileList([]);
|
|
|
|
|
|
void loadInitialData();
|
|
|
|
|
|
}, [open, initialType]);
|
2026-04-27 02:39:34 +00:00
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const loadInitialData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const [asrRes, llmRes, promptRes, hotwordRes, hotWordGroupRes, users, defaultAsr, defaultLlm, createConfigRes] = await Promise.all([
|
2026-07-02 07:19:11 +00:00
|
|
|
|
getAiModelPage({current: 1, size: 100, type: "ASR", tenantEnabledOnly: true}),
|
|
|
|
|
|
getAiModelPage({current: 1, size: 100, type: "LLM", tenantEnabledOnly: true}),
|
2026-04-08 06:34:59 +00:00
|
|
|
|
getPromptPage({ current: 1, size: 100 }),
|
|
|
|
|
|
getHotWordPage({ current: 1, size: 1000 }),
|
2026-04-27 02:39:34 +00:00
|
|
|
|
getHotWordGroupOptions(),
|
2026-04-08 06:34:59 +00:00
|
|
|
|
listUsers(),
|
|
|
|
|
|
getAiModelDefault("ASR"),
|
2026-04-24 07:47:52 +00:00
|
|
|
|
getAiModelDefault("LLM"),
|
2026-05-13 06:31:48 +00:00
|
|
|
|
getMeetingCreateConfig(),
|
2026-04-08 06:34:59 +00:00
|
|
|
|
]);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
|
|
|
|
|
const nextConfig = createConfigRes.data.data || DEFAULT_CREATE_CONFIG;
|
|
|
|
|
|
const nextType = resolveAvailableCreateType(initialType, nextConfig);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
const promptRecords = promptRes.data?.data?.records || [];
|
|
|
|
|
|
const asrRecords = asrRes.data?.data?.records || [];
|
|
|
|
|
|
const llmRecords = llmRes.data?.data?.records || [];
|
|
|
|
|
|
const hotwordRecords = hotwordRes.data?.data?.records || [];
|
|
|
|
|
|
const activePrompts = promptRecords.filter((item: PromptTemplateVO) => item.status === 1);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
|
|
|
|
|
setCreateConfig(nextConfig);
|
|
|
|
|
|
setConfigLoaded(true);
|
|
|
|
|
|
setType(nextType);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setAsrModels(asrRecords.filter((item: AiModelVO) => item.status === 1));
|
|
|
|
|
|
setLlmModels(llmRecords.filter((item: AiModelVO) => item.status === 1));
|
2026-04-08 06:34:59 +00:00
|
|
|
|
setPrompts(activePrompts);
|
2026-06-30 05:37:33 +00:00
|
|
|
|
setHotwordList(hotwordRecords.filter((item: HotWordVO) => item.status === 1));
|
2026-04-27 02:39:34 +00:00
|
|
|
|
setHotWordGroups((hotWordGroupRes.data.data || []).filter((item: HotWordGroupVO) => item.status === 1));
|
2026-04-08 06:34:59 +00:00
|
|
|
|
setUserList(users || []);
|
|
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const defaultPrompt = activePrompts[0];
|
2026-04-08 06:34:59 +00:00
|
|
|
|
form.setFieldsValue({
|
2026-05-13 06:31:48 +00:00
|
|
|
|
title: nextType === "upload" ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
2026-04-08 06:34:59 +00:00
|
|
|
|
meetingTime: dayjs(),
|
|
|
|
|
|
asrModelId: defaultAsr.data.data?.id,
|
|
|
|
|
|
summaryModelId: defaultLlm.data.data?.id,
|
2026-04-27 02:39:34 +00:00
|
|
|
|
promptId: defaultPrompt?.id,
|
2026-05-13 06:31:48 +00:00
|
|
|
|
hotWordGroupId: defaultPrompt?.hotWordGroupId ?? 0,
|
|
|
|
|
|
summaryDetailLevel: "STANDARD",
|
2026-04-08 06:34:59 +00:00
|
|
|
|
useSpkId: 1,
|
2026-06-17 07:16:08 +00:00
|
|
|
|
enableTextRefine: true,
|
2026-04-08 06:34:59 +00:00
|
|
|
|
mode: "2pass",
|
|
|
|
|
|
language: "auto",
|
|
|
|
|
|
enablePunctuation: true,
|
|
|
|
|
|
enableItn: true,
|
|
|
|
|
|
saveAudio: false,
|
|
|
|
|
|
});
|
2026-05-13 06:31:48 +00:00
|
|
|
|
} catch {
|
2026-04-08 06:34:59 +00:00
|
|
|
|
message.error("加载配置失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-05-13 06:31:48 +00:00
|
|
|
|
if (!open) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const currentTitle = form.getFieldValue("title");
|
|
|
|
|
|
if (typeof currentTitle !== "string") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentTitle.startsWith("文件会议") || currentTitle.startsWith("实时会议")) {
|
2026-04-08 06:34:59 +00:00
|
|
|
|
form.setFieldsValue({
|
2026-05-13 06:31:48 +00:00
|
|
|
|
title: type === "upload" ? `文件会议 ${dayjs().format("MM-DD HH:mm")}` : `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
2026-04-08 06:34:59 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
}, [form, open, type]);
|
|
|
|
|
|
|
|
|
|
|
|
const beforeAudioUpload = (file: File) => {
|
|
|
|
|
|
if (file.size > offlineAudioMaxSizeBytes) {
|
|
|
|
|
|
message.error(`录音文件大小不能超过 ${createConfig.offlineAudioMaxSizeMb}MB`);
|
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
|
return Upload.LIST_IGNORE;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
|
|
|
|
|
const customUpload = async (options: any) => {
|
2026-04-10 09:36:45 +00:00
|
|
|
|
const { file, onSuccess: uploadSuccess, onError, onProgress } = options;
|
2026-04-08 06:34:59 +00:00
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
|
try {
|
2026-04-10 09:36:45 +00:00
|
|
|
|
const res = await uploadAudio(file, (progressEvent) => {
|
|
|
|
|
|
if (progressEvent.total) {
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const percent = Math.min(99, Math.round((progressEvent.loaded * 100) / progressEvent.total));
|
|
|
|
|
|
setUploadProgress(percent);
|
|
|
|
|
|
onProgress({ percent });
|
2026-04-10 09:36:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-04-08 06:34:59 +00:00
|
|
|
|
setUploadProgress(100);
|
2026-04-10 09:36:45 +00:00
|
|
|
|
onProgress({ percent: 100 });
|
2026-04-08 06:34:59 +00:00
|
|
|
|
setAudioUrl(res.data.data);
|
|
|
|
|
|
uploadSuccess(res.data.data);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
message.success("录音上传成功");
|
2026-04-08 06:34:59 +00:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
onError(err);
|
2026-05-13 06:31:48 +00:00
|
|
|
|
message.error("文件上传失败");
|
2026-04-24 07:47:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
const handleOk = async () => {
|
|
|
|
|
|
if (availableTypes.length === 0) {
|
|
|
|
|
|
message.error("当前系统参数已关闭全部会议创建入口");
|
|
|
|
|
|
return;
|
2026-04-24 07:47:52 +00:00
|
|
|
|
}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
if (!availableTypes.includes(type)) {
|
|
|
|
|
|
const fallbackType = availableTypes[0];
|
|
|
|
|
|
if (fallbackType) {
|
|
|
|
|
|
setType(fallbackType);
|
|
|
|
|
|
}
|
|
|
|
|
|
message.warning("当前入口已关闭,已切换到可用创建方式");
|
|
|
|
|
|
return;
|
2026-04-08 06:34:59 +00:00
|
|
|
|
}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
if (type === "upload" && !audioUrl) {
|
|
|
|
|
|
message.error("请先上传录音文件");
|
2026-04-08 06:34:59 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const values = await form.validateFields();
|
2026-05-13 06:31:48 +00:00
|
|
|
|
if (type === "realtime") {
|
2026-04-08 06:34:59 +00:00
|
|
|
|
const wsUrl = resolveWsUrl(selectedAsrModel);
|
|
|
|
|
|
if (!wsUrl) {
|
2026-05-13 06:31:48 +00:00
|
|
|
|
message.error("当前 ASR 模型未配置 WebSocket 地址");
|
2026-04-08 06:34:59 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { hostUserId, ...meetingValues } = values;
|
2026-04-27 02:39:34 +00:00
|
|
|
|
const selectedHotWords = meetingValues.hotWordGroupId == null || meetingValues.hotWordGroupId === 0
|
|
|
|
|
|
? undefined
|
|
|
|
|
|
: hotwordList
|
|
|
|
|
|
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId)
|
|
|
|
|
|
.map((item) => item.word)
|
|
|
|
|
|
.filter((word) => !!word?.trim());
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
|
|
|
|
|
if (type === "upload") {
|
2026-04-08 06:34:59 +00:00
|
|
|
|
await createMeeting({
|
|
|
|
|
|
...meetingValues,
|
|
|
|
|
|
...(hostUserId != null ? { hostUserId } : {}),
|
|
|
|
|
|
meetingTime: meetingValues.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
2026-05-13 06:31:48 +00:00
|
|
|
|
audioUrl,
|
|
|
|
|
|
participants: meetingValues.participants?.join(","),
|
|
|
|
|
|
tags: meetingValues.tags?.join(","),
|
|
|
|
|
|
summaryDetailLevel: meetingValues.summaryDetailLevel as SummaryDetailLevel,
|
2026-04-27 02:39:34 +00:00
|
|
|
|
hotWords: selectedHotWords,
|
2026-05-13 06:31:48 +00:00
|
|
|
|
});
|
|
|
|
|
|
message.success("会议发起成功");
|
2026-04-08 06:34:59 +00:00
|
|
|
|
onSuccess();
|
|
|
|
|
|
onCancel();
|
2026-05-13 06:31:48 +00:00
|
|
|
|
return;
|
2026-04-08 06:34:59 +00:00
|
|
|
|
}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
|
|
|
|
|
const selectedHotwords = hotwordList
|
|
|
|
|
|
.filter((item) => item.hotWordGroupId === meetingValues.hotWordGroupId && meetingValues.hotWordGroupId !== 0)
|
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
|
hotword: item.word,
|
|
|
|
|
|
weight: Number(item.weight || 2) / 10,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const payload: CreateRealtimeMeetingCommand = {
|
|
|
|
|
|
...meetingValues,
|
|
|
|
|
|
...(hostUserId != null ? { hostUserId } : {}),
|
|
|
|
|
|
meetingTime: meetingValues.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
|
|
|
|
|
participants: meetingValues.participants?.join(",") || "",
|
|
|
|
|
|
tags: meetingValues.tags?.join(",") || "",
|
|
|
|
|
|
summaryDetailLevel: meetingValues.summaryDetailLevel as SummaryDetailLevel,
|
|
|
|
|
|
mode: meetingValues.mode || "2pass",
|
|
|
|
|
|
language: meetingValues.language || "auto",
|
|
|
|
|
|
useSpkId: meetingValues.useSpkId == null ? 1 : (meetingValues.useSpkId ? 1 : 0),
|
|
|
|
|
|
enablePunctuation: meetingValues.enablePunctuation !== false,
|
|
|
|
|
|
enableItn: meetingValues.enableItn !== false,
|
|
|
|
|
|
enableTextRefine: !!meetingValues.enableTextRefine,
|
|
|
|
|
|
saveAudio: !!meetingValues.saveAudio,
|
|
|
|
|
|
hotWords: selectedHotWords,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const res = await createRealtimeMeeting(payload);
|
|
|
|
|
|
const createdMeeting = res.data.data;
|
|
|
|
|
|
const sessionDraft: RealtimeMeetingSessionDraft = {
|
|
|
|
|
|
meetingId: createdMeeting.id,
|
|
|
|
|
|
meetingTitle: createdMeeting.title,
|
|
|
|
|
|
asrModelName: selectedAsrModel?.modelName || "ASR",
|
|
|
|
|
|
summaryModelName: selectedSummaryModel?.modelName || "LLM",
|
|
|
|
|
|
asrModelId: selectedAsrModel?.id || values.asrModelId,
|
|
|
|
|
|
mode: values.mode || "2pass",
|
|
|
|
|
|
language: values.language || "auto",
|
|
|
|
|
|
useSpkId: values.useSpkId == null ? 1 : (values.useSpkId ? 1 : 0),
|
|
|
|
|
|
enablePunctuation: values.enablePunctuation !== false,
|
|
|
|
|
|
enableItn: values.enableItn !== false,
|
|
|
|
|
|
enableTextRefine: !!values.enableTextRefine,
|
|
|
|
|
|
saveAudio: !!values.saveAudio,
|
|
|
|
|
|
hotwords: selectedHotwords,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
sessionStorage.setItem(getSessionKey(createdMeeting.id), JSON.stringify(sessionDraft));
|
|
|
|
|
|
message.success("会议已创建,即将进入实时识别");
|
|
|
|
|
|
onSuccess();
|
|
|
|
|
|
onCancel();
|
|
|
|
|
|
navigate(`/meeting-live-session/${createdMeeting.id}`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error(type === "upload" ? "创建会议失败" : "创建实时会议失败");
|
2026-04-08 06:34:59 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Drawer
|
2026-06-30 05:37:33 +00:00
|
|
|
|
rootClassName="meeting-create-drawer-root"
|
|
|
|
|
|
className="meeting-create-drawer"
|
2026-05-13 06:31:48 +00:00
|
|
|
|
title={null}
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
onClose={onCancel}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
width="min(960px, calc(100vw - 48px))"
|
2026-05-13 06:31:48 +00:00
|
|
|
|
forceRender
|
|
|
|
|
|
destroyOnClose={false}
|
|
|
|
|
|
placement="right"
|
|
|
|
|
|
closable={false}
|
|
|
|
|
|
footer={
|
|
|
|
|
|
configLoaded ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-drawer__footer">
|
|
|
|
|
|
<Space size={12}>
|
|
|
|
|
|
<Button onClick={onCancel}>取消</Button>
|
|
|
|
|
|
<Button type="primary" onClick={handleOk} loading={submitting}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
{type === "upload" ? (audioUrl ? "完成并分析" : "上传并分析") : "创建并进入识别"}
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
header: { display: "none" },
|
|
|
|
|
|
body: { padding: 0, display: "flex", flexDirection: "column", background: "var(--app-bg-layout)" },
|
|
|
|
|
|
footer: { padding: 0, borderTop: "1px solid var(--app-border-color)", background: "var(--app-bg-surface)", minHeight: configLoaded ? 72 : 0 }
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{!configLoaded ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-drawer__skeleton">
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Skeleton active paragraph={{ rows: 12 }} />
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</div>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-drawer__header">
|
|
|
|
|
|
<Row justify="space-between" align="middle" gutter={[16, 12]}>
|
|
|
|
|
|
<Col flex="auto">
|
|
|
|
|
|
<Space size={12}>
|
|
|
|
|
|
<div className="meeting-create-drawer__title-icon">
|
2026-05-13 06:31:48 +00:00
|
|
|
|
{type === "upload" ? <CloudUploadOutlined /> : <AudioOutlined />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Title level={4} className="meeting-create-drawer__title">{type === "upload" ? "上传录音发起分析" : "创建实时会议"}</Title>
|
|
|
|
|
|
<Text type="secondary" className="meeting-create-drawer__subtitle">{type === "upload" ? "上传已有音频文件并由 AI 进行转写与总结分析" : "实时采集语音并进行流式转写与实时纪要生成"}</Text>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Col>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col flex="none">
|
|
|
|
|
|
<Radio.Group value={type} onChange={e => setType(e.target.value)} optionType="button" buttonStyle="solid" className="meeting-create-drawer__type-switch">
|
2026-05-13 06:31:48 +00:00
|
|
|
|
{createConfig.offlineEnabled && (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Radio.Button value="upload"><CloudUploadOutlined /> 上传录音</Radio.Button>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
)}
|
|
|
|
|
|
{createConfig.realtimeEnabled && (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Radio.Button value="realtime"><AudioOutlined /> 实时会议</Radio.Button>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-drawer__body">
|
|
|
|
|
|
<Form form={form} layout="vertical" disabled={loading} className="meeting-create-form">
|
|
|
|
|
|
<div className="meeting-create-section-title">
|
|
|
|
|
|
<span className="meeting-create-section-title__bar" />
|
|
|
|
|
|
<Title level={5}>基础信息</Title>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="title" label="会议标题" rules={[{ required: true }]}>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Input placeholder="输入会议标题" />
|
2026-05-13 06:31:48 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} md={12}>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Form.Item name="meetingTime" label="会议时间" rules={[{ required: true }]}>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<DatePicker showTime style={{ width: "100%" }} />
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Form.Item name="participants" label="参会人员">
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select mode="multiple" placeholder="选择参会人员" showSearch optionFilterProp="children">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
{userList.map(u => (<Option key={u.userId} value={u.userId}><Space><Avatar size="small" icon={<UserOutlined />} />{u.displayName || u.username}</Space></Option>))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} md={12}>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Form.Item name="hostUserId" label="会议主持人">
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select allowClear placeholder="默认为创建人" showSearch optionFilterProp="children">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
{userList.map(u => (<Option key={u.userId} value={u.userId}><Space><Avatar size="small" icon={<UserOutlined />} />{u.displayName || u.username}</Space></Option>))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col span={24}>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Form.Item name="tags" label="会议标签">
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select mode="tags" placeholder="输入标签" />
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-section-divider" />
|
2026-04-08 06:34:59 +00:00
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-section-title">
|
|
|
|
|
|
<span className="meeting-create-section-title__bar" />
|
|
|
|
|
|
<Title level={5}>模型与纪要配置</Title>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</div>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="asrModelId" label="语音识别模型 (ASR)" rules={[{ required: true }]}>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select placeholder="选择 ASR 模型">{asrModels.map(m => (<Option key={m.id} value={m.id}>{m.modelName}</Option>))}</Select>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} md={12}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="summaryModelId" label="总结模型 (LLM)" rules={[{ required: true }]}>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select placeholder="选择总结模型">{llmModels.map(m => (<Option key={m.id} value={m.id}>{m.modelName}</Option>))}</Select>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="promptId" label="总结模板" rules={[{ required: true }]}>
|
|
|
|
|
|
{prompts.length > 15 ? (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select placeholder="请选择总结模板" showSearch optionFilterProp="children">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
{prompts.map(p => <Option key={p.id} value={p.id}>{p.templateName}</Option>)}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
) : (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-template-grid">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Row gutter={[12, 12]}>
|
|
|
|
|
|
{prompts.map(p => {
|
|
|
|
|
|
const isSelected = watchedPromptId === p.id;
|
|
|
|
|
|
return (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} sm={12} md={8} key={p.id}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
onClick={() => form.setFieldsValue({ promptId: p.id })}
|
|
|
|
|
|
className={isSelected ? "meeting-create-template-card is-selected" : "meeting-create-template-card"}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="meeting-create-template-card__name">{p.templateName}</div>
|
|
|
|
|
|
{isSelected && <div className="meeting-create-template-card__check"><CheckOutlined /></div>}
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="hotWordGroupId" label="热词组" tooltip={selectedPrompt?.hotWordGroupName ? `默认跟随模板:${selectedPrompt.hotWordGroupName}` : "模板未绑定热词组时可手动选择"} extra={watchedHotWordGroupId != null ? "创建会议时会优先使用这里选中的热词组" : undefined}>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Select placeholder={selectedPrompt?.hotWordGroupId ? "默认已带出模板热词组,可按需修改" : "请选择热词组"} options={[{ label: "不使用热词组", value: 0 }, ...hotWordGroups.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))]} />
|
2026-05-13 06:31:48 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} md={12}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="summaryDetailLevel" label="总结详细程度" rules={[{ required: true }]}>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Radio.Group className="meeting-create-radio-line">
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Radio value="DETAILED">详细</Radio>
|
|
|
|
|
|
<Radio value="STANDARD">标准</Radio>
|
|
|
|
|
|
<Radio value="BRIEF">简洁</Radio>
|
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-04-27 02:39:34 +00:00
|
|
|
|
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Collapse
|
|
|
|
|
|
ghost
|
|
|
|
|
|
expandIconPosition="end"
|
2026-06-30 05:37:33 +00:00
|
|
|
|
className="meeting-create-advanced"
|
2026-04-08 06:34:59 +00:00
|
|
|
|
items={[
|
|
|
|
|
|
{
|
2026-05-13 06:31:48 +00:00
|
|
|
|
key: "advanced",
|
2026-04-10 01:14:00 +00:00
|
|
|
|
forceRender: true,
|
2026-04-08 06:34:59 +00:00
|
|
|
|
label: (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-advanced__label">
|
|
|
|
|
|
<div className="meeting-create-advanced__title">
|
|
|
|
|
|
<div className="meeting-create-advanced__icon">
|
|
|
|
|
|
<SettingOutlined />
|
2026-04-08 09:30:31 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
高级设置
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
),
|
|
|
|
|
|
children: (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-advanced__body">
|
|
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
|
<Col xs={24} md={8}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="useSpkId" label={<span>说话人区分 <Tooltip title="自动识别录音中的不同发言者并进行角色分离"><QuestionCircleOutlined /></Tooltip></span>} valuePropName="checked" getValueProps={(v) => ({ checked: !!v })} normalize={(v) => (v ? 1 : 0)}>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} md={8}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="enableTextRefine" label={<span>文本修正 <Tooltip title="自动识别并修正语音转写中的口语词、语气助词等"><QuestionCircleOutlined /></Tooltip></span>} valuePropName="checked">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
{type === "realtime" && (
|
|
|
|
|
|
<>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Col xs={24} md={8}>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="saveAudio" label="保存录音" valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={24}>
|
|
|
|
|
|
<Form.Item name="mode" label="识别模式">
|
|
|
|
|
|
<Radio.Group>
|
|
|
|
|
|
<Radio value="2pass">2pass(流式 + 离线修正)</Radio>
|
|
|
|
|
|
<Radio value="online">online(纯流式)</Radio>
|
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</Row>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Form.Item name="userPrompt" label="用户提示词" extra="提供补充信息以指导 AI 在生成会议总结时更侧重于特定方面或遵循特定风格">
|
|
|
|
|
|
<Input.TextArea placeholder="请输入额外说明,例如:请重点关注会议中的待办事项、以摘要形式输出、或者采用严谨的公文风格等" autoSize={{ minRows: 3, maxRows: 6 }} showCount maxLength={1000} />
|
2026-04-17 02:08:40 +00:00
|
|
|
|
</Form.Item>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-13 06:31:48 +00:00
|
|
|
|
{type === "upload" && (
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-section-divider" />
|
|
|
|
|
|
<div className="meeting-create-section-title">
|
|
|
|
|
|
<span className="meeting-create-section-title__bar" />
|
|
|
|
|
|
<Title level={5}>上传录音文件</Title>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</div>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<Dragger
|
|
|
|
|
|
accept=".mp3,.wav,.m4a"
|
2026-04-24 07:47:52 +00:00
|
|
|
|
beforeUpload={beforeAudioUpload}
|
2026-05-13 06:31:48 +00:00
|
|
|
|
fileList={fileList}
|
|
|
|
|
|
customRequest={customUpload}
|
|
|
|
|
|
onChange={info => setFileList(info.fileList.slice(-1))}
|
|
|
|
|
|
maxCount={1}
|
2026-06-30 05:37:33 +00:00
|
|
|
|
className="meeting-create-upload"
|
2026-04-08 06:34:59 +00:00
|
|
|
|
>
|
|
|
|
|
|
<div>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<p className="ant-upload-drag-icon"><CloudUploadOutlined /></p>
|
|
|
|
|
|
<p className="ant-upload-text">点击或拖拽录音文件到此处</p>
|
|
|
|
|
|
<p className="ant-upload-hint">支持 mp3、wav、m4a 等格式,大小不超过 {createConfig.offlineAudioMaxSizeMb}MB</p>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div className="meeting-create-upload__progress">
|
2026-04-08 06:34:59 +00:00
|
|
|
|
<Progress percent={uploadProgress} size="small" />
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<div>文件上传中,请稍候...</div>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{audioUrl && (
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<Tag color="processing" className="meeting-create-upload__file">
|
2026-05-13 06:31:48 +00:00
|
|
|
|
<span>已上传:</span>
|
2026-06-30 05:37:33 +00:00
|
|
|
|
<span>{audioUrl.split("/").pop()}</span>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Dragger>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
2026-05-13 06:31:48 +00:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Drawer>
|
2026-04-08 06:34:59 +00:00
|
|
|
|
);
|
2026-06-17 07:16:08 +00:00
|
|
|
|
};
|