feat:优化会议转录和章节功能

- 更新 `MeetingTranscriptFileServiceImpl`,使用 Markdown 格式导出会议转录
- 在 `MeetingQueryServiceImpl` 中添加会议存在性检查,并调用 `listDisplayChapterAnalysis`
- 在 `MeetingCommandServiceImpl` 中更新会议进度信息
- 在前端 `http.ts` 和 `meeting.ts` 中添加 `suppressErrorToast` 选项
- 在 `MeetingVO` 中添加最近一次总结和章节尝试的任务状态及错误信息
- 更新 `MeetingDetail.tsx` 和 `Meetings.tsx`,处理生成失败的提示和展示逻辑
- 在 `MeetingTranscriptChapterService` 和 `MeetingTranscriptChapterServiceImpl` 中添加加载当前章节 Markdown 的方法
- 优化 `PageContainer` 和 `Meetings` 页面布局,改善滚动和内容展示
dev_na
chenhao 2026-05-11 10:54:33 +08:00
parent 1877c64cc2
commit ccb408ade5
13 changed files with 317 additions and 48 deletions

View File

@ -57,6 +57,18 @@ public class MeetingVO {
private String lastUserPrompt;
@Schema(description = "分析结果")
private Map<String, Object> analysis;
@Schema(description = "最近一次总结尝试任务 ID")
private Long latestSummaryAttemptTaskId;
@Schema(description = "最近一次总结尝试任务状态")
private Integer latestSummaryAttemptStatus;
@Schema(description = "最近一次总结尝试错误信息")
private String latestSummaryAttemptErrorMsg;
@Schema(description = "最近一次章节尝试任务 ID")
private Long latestChapterAttemptTaskId;
@Schema(description = "最近一次章节尝试任务状态")
private Integer latestChapterAttemptStatus;
@Schema(description = "最近一次章节尝试错误信息")
private String latestChapterAttemptErrorMsg;
@Schema(description = "会议状态")
private Integer status;

View File

@ -15,6 +15,8 @@ public interface MeetingTranscriptChapterService {
List<Map<String, Object>> listCurrentChapterAnalysis(Long meetingId);
List<Map<String, Object>> listDisplayChapterAnalysis(Meeting meeting);
void invalidateCurrentVersion(Long meetingId);
MeetingTranscriptChapterVersion importExternalChapters(Meeting meeting, AiTask sourceTask, MeetingTranscriptChapterImportDTO command);
@ -22,4 +24,6 @@ public interface MeetingTranscriptChapterService {
MeetingTranscriptSourceVO buildTranscriptSource(Long meetingId);
MeetingTranscriptChapterVersion getCurrentVersion(Long meetingId);
String loadCurrentChapterMarkdown(Meeting meeting);
}

View File

@ -8,4 +8,6 @@ public interface MeetingTranscriptFileService {
void initializeTranscriptFileIfAbsent(Long meetingId);
MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail);
String loadTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail);
}

View File

@ -735,6 +735,11 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
}
meeting.setStatus(2);
meetingService.updateById(meeting);
if ("EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode)) {
updateMeetingProgress(meetingId, 95, "等待外部章节与总结编排...", 0);
} else {
updateMeetingProgress(meetingId, 85, "重新总结已提交,正在生成章节...", 0);
}
dispatchSummaryTaskAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
}

View File

@ -339,6 +339,7 @@ public class MeetingDomainSupport {
vo.setSummaryContent(meetingSummaryFileService.loadSummaryContent(meeting));
vo.setAnalysis(meetingSummaryFileService.loadSummaryAnalysis(meeting));
vo.setLastUserPrompt(resolveLastSummaryUserPrompt(meeting));
fillLatestTaskAttemptInfo(meeting, vo);
}
}
@ -379,6 +380,41 @@ public class MeetingDomainSupport {
.last("LIMIT 1"));
}
private void fillLatestTaskAttemptInfo(Meeting meeting, com.imeeting.dto.biz.MeetingVO vo) {
AiTask latestSummaryAttempt = resolveLatestTaskAttempt(meeting, "SUMMARY");
if (latestSummaryAttempt != null) {
vo.setLatestSummaryAttemptTaskId(latestSummaryAttempt.getId());
vo.setLatestSummaryAttemptStatus(latestSummaryAttempt.getStatus());
vo.setLatestSummaryAttemptErrorMsg(normalizeTaskError(latestSummaryAttempt.getErrorMsg()));
}
AiTask latestChapterAttempt = resolveLatestTaskAttempt(meeting, "CHAPTER");
if (latestChapterAttempt != null) {
vo.setLatestChapterAttemptTaskId(latestChapterAttempt.getId());
vo.setLatestChapterAttemptStatus(latestChapterAttempt.getStatus());
vo.setLatestChapterAttemptErrorMsg(normalizeTaskError(latestChapterAttempt.getErrorMsg()));
}
}
private AiTask resolveLatestTaskAttempt(Meeting meeting, String taskType) {
if (meeting == null || meeting.getId() == null) {
return null;
}
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meeting.getId())
.eq(AiTask::getTaskType, taskType)
.orderByDesc(AiTask::getId)
.last("LIMIT 1"));
}
private String normalizeTaskError(String errorMsg) {
if (errorMsg == null) {
return null;
}
String normalized = errorMsg.trim();
return normalized.isEmpty() ? null : normalized;
}
private record AudioRelocationPlan(Path sourcePath, Path targetPath, Path backupPath, String relocatedUrl) {
}
}

View File

@ -105,7 +105,11 @@ public class MeetingQueryServiceImpl implements MeetingQueryService {
@Override
public List<Map<String, Object>> getChapters(Long meetingId) {
return meetingTranscriptChapterService.listCurrentChapterAnalysis(meetingId);
Meeting meeting = meetingService.getById(meetingId);
if (meeting == null) {
return List.of();
}
return meetingTranscriptChapterService.listDisplayChapterAnalysis(meeting);
}
@Override

View File

@ -15,6 +15,7 @@ import com.imeeting.entity.biz.Meeting;
import com.imeeting.entity.biz.MeetingTranscript;
import com.imeeting.entity.biz.MeetingTranscriptChapter;
import com.imeeting.entity.biz.MeetingTranscriptChapterVersion;
import com.imeeting.mapper.biz.AiTaskMapper;
import com.imeeting.mapper.biz.MeetingTranscriptChapterMapper;
import com.imeeting.mapper.biz.MeetingTranscriptChapterVersionMapper;
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
@ -71,6 +72,7 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
private final MeetingTranscriptMapper transcriptMapper;
private final MeetingTranscriptChapterVersionMapper versionMapper;
private final MeetingTranscriptChapterMapper chapterMapper;
private final AiTaskMapper aiTaskMapper;
private final ObjectMapper objectMapper;
private AiModelService aiModelService;
@ -129,10 +131,26 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
if (current == null) {
return List.of();
}
return listVersionChapterAnalysis(meetingId, current.getId());
}
@Override
public List<Map<String, Object>> listDisplayChapterAnalysis(Meeting meeting) {
if (meeting == null || meeting.getId() == null) {
return List.of();
}
MeetingTranscriptChapterVersion displayVersion = resolveDisplayVersion(meeting);
if (displayVersion == null) {
return List.of();
}
return listVersionChapterAnalysis(meeting.getId(), displayVersion.getId());
}
private List<Map<String, Object>> listVersionChapterAnalysis(Long meetingId, Long versionId) {
List<MeetingTranscript> transcripts = loadRawTranscripts(meetingId);
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
return loadVersionChapters(current.getId()).stream()
return loadVersionChapters(versionId).stream()
.map(chapter -> toChapterAnalysis(chapter, transcriptById))
.toList();
}
@ -200,6 +218,29 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
return findCurrentVersion(meetingId);
}
@Override
public String loadCurrentChapterMarkdown(Meeting meeting) {
if (meeting == null || meeting.getId() == null) {
throw new RuntimeException("Meeting not found");
}
MeetingTranscriptChapterVersion current = findCurrentVersion(meeting.getId());
if (current == null) {
return "";
}
List<MeetingTranscript> transcripts = loadRawTranscripts(meeting.getId());
Map<Long, MeetingTranscript> transcriptById = transcripts.stream()
.collect(Collectors.toMap(MeetingTranscript::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
List<MeetingTranscriptChapter> chapters = loadVersionChapters(current.getId());
String relativePath = writeCurrentChapterMarkdown(meeting, current, chapters, transcriptById);
try {
String basePath = uploadPath.endsWith("/") || uploadPath.endsWith("\\") ? uploadPath : uploadPath + "/";
Path targetPath = Paths.get(basePath, relativePath.replace("\\", "/"));
return Files.exists(targetPath) ? Files.readString(targetPath, StandardCharsets.UTF_8) : "";
} catch (Exception ex) {
throw new RuntimeException("Failed to load meeting chapter markdown", ex);
}
}
private MeetingTranscriptChapterVersion generateInternalVersion(Meeting meeting,
AiTask summaryTask,
List<MeetingTranscript> transcripts,
@ -397,6 +438,49 @@ public class MeetingTranscriptChapterServiceImpl implements MeetingTranscriptCha
return version;
}
private MeetingTranscriptChapterVersion resolveDisplayVersion(Meeting meeting) {
Long chapterVersionId = extractChapterVersionId(resolveDisplaySummaryTask(meeting));
if (chapterVersionId != null) {
MeetingTranscriptChapterVersion version = versionMapper.selectById(chapterVersionId);
if (version != null && Objects.equals(version.getMeetingId(), meeting.getId())) {
return version;
}
}
return findCurrentVersion(meeting.getId());
}
private AiTask resolveDisplaySummaryTask(Meeting meeting) {
if (meeting.getLatestSummaryTaskId() != null) {
AiTask task = aiTaskMapper.selectById(meeting.getLatestSummaryTaskId());
if (task != null
&& Objects.equals(task.getMeetingId(), meeting.getId())
&& "SUMMARY".equals(task.getTaskType())
&& Integer.valueOf(2).equals(task.getStatus())) {
return task;
}
}
return aiTaskMapper.selectOne(new LambdaQueryWrapper<AiTask>()
.eq(AiTask::getMeetingId, meeting.getId())
.eq(AiTask::getTaskType, "SUMMARY")
.eq(AiTask::getStatus, 2)
.orderByDesc(AiTask::getId)
.last("LIMIT 1"));
}
private Long extractChapterVersionId(AiTask summaryTask) {
if (summaryTask == null || summaryTask.getResponseData() == null) {
return null;
}
Object summarySource = summaryTask.getResponseData().get("summarySource");
if (summarySource instanceof Map<?, ?> sourceMap) {
Long versionId = longValue(sourceMap.get("chapterVersionId"));
if (versionId != null) {
return versionId;
}
}
return longValue(summaryTask.getResponseData().get("chapterVersionId"));
}
private MeetingSummarySource buildSummarySource(Meeting meeting,
MeetingTranscriptChapterVersion version,
List<MeetingTranscript> transcripts,

View File

@ -49,15 +49,16 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
if (meeting == null) {
return;
}
writeTranscriptFile(meeting, null);
loadTranscriptMarkdown(meeting, null);
}
@Override
public MeetingTranscriptExportResult exportTranscript(Meeting meeting, MeetingVO meetingDetail) {
if (meeting == null || meeting.getId() == null) {
throw new RuntimeException("会议不存在");
throw new RuntimeException("Meeting not found");
}
byte[] content = writeTranscriptFile(meeting, meetingDetail);
String markdown = loadTranscriptMarkdown(meeting, meetingDetail);
byte[] content = markdown.getBytes(StandardCharsets.UTF_8);
String safeTitle = sanitizeFileName(
meetingDetail != null ? meetingDetail.getTitle() : meeting.getTitle(),
"meeting-transcript-" + meeting.getId()
@ -65,7 +66,8 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
return new MeetingTranscriptExportResult(content, CONTENT_TYPE, safeTitle + "-Transcript.md");
}
private byte[] writeTranscriptFile(Meeting meeting, MeetingVO meetingDetail) {
@Override
public String loadTranscriptMarkdown(Meeting meeting, MeetingVO meetingDetail) {
try {
Path transcriptPath = buildTranscriptPath(meeting.getId());
Path parent = transcriptPath.getParent();
@ -74,9 +76,9 @@ public class MeetingTranscriptFileServiceImpl implements MeetingTranscriptFileSe
}
String markdown = buildTranscriptMarkdown(meeting, meetingDetail);
Files.writeString(transcriptPath, markdown, StandardCharsets.UTF_8);
return markdown.getBytes(StandardCharsets.UTF_8);
return markdown;
} catch (IOException ex) {
throw new RuntimeException("写入会议转录文件失败", ex);
throw new RuntimeException("Failed to write meeting transcript markdown", ex);
}
}

View File

@ -33,6 +33,12 @@ export interface MeetingVO {
keyPoints?: Array<{ title?: string; summary?: string; speaker?: string; time?: string }>;
todos?: string[];
};
latestSummaryAttemptTaskId?: number;
latestSummaryAttemptStatus?: number;
latestSummaryAttemptErrorMsg?: string;
latestChapterAttemptTaskId?: number;
latestChapterAttemptStatus?: number;
latestChapterAttemptErrorMsg?: string;
status: number;
displayStatus?: number;
realtimeSessionStatus?: RealtimeMeetingSessionStatus["status"];
@ -269,11 +275,12 @@ export interface PublicMeetingPreviewVO {
chapters?: MeetingChapterVO[];
}
export const getMeetingDetail = (id: number) => {
export const getMeetingDetail = (id: number, options?: { suppressErrorToast?: boolean }) => {
return http.get<{ code: string; data: MeetingVO; msg: string }>(
`/api/biz/meeting/${id}`,
{
timeout: MEETING_DETAIL_TIMEOUT,
suppressErrorToast: options?.suppressErrorToast,
}
);
};
@ -403,9 +410,12 @@ export interface MeetingProgress {
eta?: number;
}
export const getMeetingProgress = (id: number) => {
export const getMeetingProgress = (id: number, options?: { suppressErrorToast?: boolean }) => {
return http.get<{ code: string; data: MeetingProgress; msg: string }>(
`/api/biz/meeting/${id}/progress`
`/api/biz/meeting/${id}/progress`,
{
suppressErrorToast: options?.suppressErrorToast,
}
);
};

View File

@ -1,6 +1,16 @@
import axios from "axios";
import { message } from "antd";
declare module "axios" {
interface AxiosRequestConfig {
suppressErrorToast?: boolean;
}
interface InternalAxiosRequestConfig {
suppressErrorToast?: boolean;
}
}
const http = axios.create({
baseURL: "/",
timeout: 15000
@ -122,7 +132,9 @@ http.interceptors.response.use(
const body = resp.data;
if (body && !isApiSuccessCode(body.code)) {
const errorMsg = body.msg || "请求失败";
message.error(errorMsg);
if (!resp.config?.suppressErrorToast) {
message.error(errorMsg);
}
const err = new Error(errorMsg);
(err as any).code = body.code;
(err as any).msg = body.msg;
@ -154,7 +166,9 @@ http.interceptors.response.use(
const body = error.response?.data;
const errorMsg = body?.msg || error.message || "网络异常";
message.error(errorMsg);
if (!originalRequest.suppressErrorToast) {
message.error(errorMsg);
}
if (body && body.msg) {
const err = new Error(body.msg);

View File

@ -85,7 +85,6 @@ const PageContainer: React.FC<PageContainerProps> = ({
style={{
flex: 1,
minHeight: 0,
overflow: 'auto',
display: 'flex',
flexDirection: 'column'
}}

View File

@ -359,20 +359,31 @@ const MeetingProgressDisplay: React.FC<{
const [progress, setProgress] = useState<MeetingProgress | null>(null);
useEffect(() => {
let completed = false;
const fetchProgress = async () => {
if (completed) {
return;
}
try {
const [progressRes, detailRes] = await Promise.all([
getMeetingProgress(meetingId),
getMeetingDetail(meetingId),
getMeetingProgress(meetingId, { suppressErrorToast: true }),
getMeetingDetail(meetingId, { suppressErrorToast: true }),
]);
if (detailRes.data?.data) {
onProgressUpdate?.(detailRes.data.data);
if (detailRes.data.data.status !== 1 && detailRes.data.data.status !== 2) {
completed = true;
onComplete();
return;
}
}
if (progressRes.data?.data) {
setProgress(progressRes.data.data);
if (progressRes.data.data.percent === 100) {
if (progressRes.data.data.percent === 100 || progressRes.data.data.percent < 0) {
completed = true;
onComplete();
}
}
@ -383,7 +394,10 @@ const MeetingProgressDisplay: React.FC<{
fetchProgress();
const timer = setInterval(fetchProgress, 3000);
return () => clearInterval(timer);
return () => {
completed = true;
clearInterval(timer);
};
}, [meetingId, onComplete, onProgressUpdate]);
const percent = progress?.percent || 0;
@ -902,6 +916,38 @@ const MeetingDetail: React.FC = () => {
const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2;
const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl;
const generationFailureNotice = useMemo(() => {
if (!meeting || meeting.status !== 4) {
return null;
}
const hasFallbackContent = Boolean(meeting.summaryContent) || meetingChapters.length > 0;
if (meeting.latestChapterAttemptStatus === 3) {
const detail = meeting.latestChapterAttemptErrorMsg || '章节生成失败';
return {
key: `chapter-${meeting.latestChapterAttemptTaskId ?? 'latest'}`,
title: '本次重新总结失败',
description: hasFallbackContent
? `章节生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
: `章节生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
hasFallbackContent,
};
}
if (meeting.latestSummaryAttemptStatus === 3) {
const detail = meeting.latestSummaryAttemptErrorMsg || '总结生成失败';
return {
key: `summary-${meeting.latestSummaryAttemptTaskId ?? 'latest'}`,
title: '本次重新总结失败',
description: hasFallbackContent
? `总结生成失败,当前展示的是上一次成功的摘要和 AI 目录。失败原因:${detail}`
: `总结生成失败,且当前没有可展示的历史摘要或 AI 目录。失败原因:${detail}`,
hasFallbackContent,
};
}
return null;
}, [meeting, meetingChapters.length]);
const emptyTranscriptFailureNotice = useMemo(() => {
if (!meeting || meeting.status !== 4 || transcripts.length > 0) {
return null;
@ -916,6 +962,24 @@ const MeetingDetail: React.FC = () => {
};
}, [canRetryTranscription, meeting, transcripts.length]);
useEffect(() => {
if (!generationFailureNotice) {
return;
}
const acknowledgedKey = `meeting-failure-ack:${generationFailureNotice.key}`;
if (sessionStorage.getItem(acknowledgedKey) === '1') {
return;
}
Modal.warning({
title: generationFailureNotice.title,
content: generationFailureNotice.description,
okText: '我知道了',
onOk: () => {
sessionStorage.setItem(acknowledgedKey, '1');
},
});
}, [generationFailureNotice]);
useEffect(() => {
if (!playbackAudioUrl) {
setShowFloatingTranscriptPlayer(false);
@ -1831,11 +1895,26 @@ const MeetingDetail: React.FC = () => {
</div>
</div>
{generationFailureNotice && (
<Alert
type="warning"
showIcon
style={{ marginBottom: 16 }}
message={generationFailureNotice.title}
description={generationFailureNotice.description}
/>
)}
{meeting.status === 2 ? (
<div className="summary-progress-shell">
<MeetingProgressDisplay
meetingId={meeting.id}
onComplete={() => fetchData(meeting.id)}
onProgressUpdate={(updated) => {
if (updated.status !== meeting.status) {
void fetchData(updated.id);
}
}}
compact
/>
</div>
@ -2029,6 +2108,15 @@ const MeetingDetail: React.FC = () => {
<div className="transcript-scroll-shell">
{workspaceTab === 'catalog' ? (
<div className="catalog-list">
{generationFailureNotice && !generationFailureNotice.hasFallbackContent && (
<Alert
type="warning"
showIcon
style={{ marginBottom: 16 }}
message="AI 目录生成失败"
description={generationFailureNotice.description}
/>
)}
{catalogChapterLinks.length ? (
catalogChapterLinks.map((chapter, index) => (
<div

View File

@ -138,10 +138,10 @@ const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => {
if (meeting.status !== 1 && meeting.status !== 2) return;
const fetchProgress = async () => {
try {
const res = await getMeetingProgress(meeting.id);
const res = await getMeetingProgress(meeting.id, { suppressErrorToast: true });
if (res.data && res.data.data) {
setProgress(res.data.data);
if (res.data.data.percent === 100 && onComplete) {
if ((res.data.data.percent === 100 || res.data.data.percent < 0) && onComplete) {
onComplete();
}
}
@ -604,35 +604,44 @@ const Meetings: React.FC = () => {
</>
}
>
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: '16px 24px', flex: 1 } }}>
{displayMode === 'card' ? (
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
<List
grid={{ gutter: [24, 24], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
dataSource={data}
renderItem={(item) => {
const config = statusConfig[item.displayStatus ?? item.status] || statusConfig[0];
return <MeetingCardItem item={item} config={config} fetchData={fetchData} t={t} onEditParticipants={openEditParticipants} onOpenMeeting={handleOpenMeeting} />;
}}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
/>
</Skeleton>
) : (
<Table
columns={tableColumns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={false}
onRow={(record) => ({
onClick: () => handleOpenMeeting(record),
style: { cursor: 'pointer' }
})}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
/>
)}
<Card
className="app-page__content-card"
style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}
>
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px 24px' }}>
{displayMode === 'card' ? (
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
<List
grid={{ gutter: [24, 24], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
dataSource={data}
renderItem={(item) => {
const config = statusConfig[item.displayStatus ?? item.status] || statusConfig[0];
return <MeetingCardItem item={item} config={config} fetchData={fetchData} t={t} onEditParticipants={openEditParticipants} onOpenMeeting={handleOpenMeeting} />;
}}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
/>
</Skeleton>
) : (
<Table
columns={tableColumns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={false}
scroll={{ x: 'max-content' }}
onRow={(record) => ({
onClick: () => handleOpenMeeting(record),
style: { cursor: 'pointer' }
})}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
/>
)}
</div>
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
<div style={{ padding: '0 24px 16px', flexShrink: 0 }}>
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
</div>
</Card>
<MeetingCreateDrawer