feat: 添加实时会议状态处理和转录内容检查
- 在 `AiTaskServiceImpl` 中添加 `buildTranscriptText` 和 `failPendingSummaryTask` 方法,用于构建转录文本和处理失败的摘要任务 - 更新 `doDispatchSummaryTask` 和 `dispatchTasks` 方法,以在转录内容为空时处理失败情况 - 在前端 `Meetings.tsx` 中添加实时会议状态处理逻辑,支持实时会议的暂停、进行中和待开始状态 - 更新测试类 `AiTaskServiceImplTest` 以包含新的测试用例,验证转录内容为空时的任务处理逻辑dev_na
parent
29551dfbe2
commit
8d0ef246f3
|
|
@ -103,9 +103,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
||||
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||
.orderByAsc(MeetingTranscript::getStartTime));
|
||||
asrText = transcripts.stream()
|
||||
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
||||
.collect(Collectors.joining("\n"));
|
||||
asrText = buildTranscriptText(transcripts);
|
||||
}
|
||||
|
||||
// Real-time meetings are created without audio files and without ASR tasks.
|
||||
|
|
@ -123,6 +121,12 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("limit 1"));
|
||||
if (asrText == null || asrText.isBlank()) {
|
||||
failPendingSummaryTask(sumTask, "No transcript content available for summary");
|
||||
updateMeetingStatus(meetingId, 4);
|
||||
updateProgress(meetingId, -1, "未识别到可用于总结的转录内容", 0);
|
||||
return;
|
||||
}
|
||||
if (sumTask != null && sumTask.getStatus() == 0) {
|
||||
processSummaryTask(meeting, asrText, sumTask);
|
||||
} else if (meeting.getStatus() != 3) {
|
||||
|
|
@ -132,6 +136,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
||||
} catch (Exception e) {
|
||||
log.error("Meeting {} AI Task Flow failed", meetingId, e);
|
||||
failPendingSummaryTask(findLatestSummaryTask(meetingId), "Summary skipped because transcription failed: " + e.getMessage());
|
||||
updateMeetingStatus(meetingId, 4);
|
||||
updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
|
||||
} finally {
|
||||
|
|
@ -148,24 +153,26 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private void doDispatchSummaryTask(Long meetingId) {
|
||||
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||
if (meeting == null) return;
|
||||
AiTask sumTask = this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("limit 1"));
|
||||
try {
|
||||
List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
||||
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||
.orderByAsc(MeetingTranscript::getStartTime));
|
||||
|
||||
if (transcripts.isEmpty()) {
|
||||
failPendingSummaryTask(sumTask, "No transcript content available for summary");
|
||||
throw new RuntimeException("没有找到可用的转录文本,无法生成总结");
|
||||
}
|
||||
|
||||
String asrText = transcripts.stream()
|
||||
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
AiTask sumTask = this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("limit 1"));
|
||||
String asrText = buildTranscriptText(transcripts);
|
||||
if (asrText == null || asrText.isBlank()) {
|
||||
failPendingSummaryTask(sumTask, "No transcript content available for summary");
|
||||
throw new RuntimeException("No transcript content available for summary");
|
||||
}
|
||||
if (sumTask != null && sumTask.getStatus() == 0) {
|
||||
processSummaryTask(meeting, asrText, sumTask);
|
||||
}
|
||||
|
|
@ -418,6 +425,47 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
return (int) Math.round(node.asDouble() * 1000D);
|
||||
}
|
||||
|
||||
private String buildTranscriptText(List<MeetingTranscript> transcripts) {
|
||||
if (transcripts == null || transcripts.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return transcripts.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(this::formatTranscriptLine)
|
||||
.filter(line -> line != null && !line.isBlank())
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
private String formatTranscriptLine(MeetingTranscript transcript) {
|
||||
String content = transcript.getContent();
|
||||
if (content == null || content.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String speaker = transcript.getSpeakerName();
|
||||
if (speaker == null || speaker.isBlank()) {
|
||||
speaker = transcript.getSpeakerId();
|
||||
}
|
||||
if (speaker == null || speaker.isBlank()) {
|
||||
return content.trim();
|
||||
}
|
||||
return speaker.trim() + ": " + content.trim();
|
||||
}
|
||||
|
||||
private void failPendingSummaryTask(AiTask task, String error) {
|
||||
if (task == null || Integer.valueOf(2).equals(task.getStatus()) || Integer.valueOf(3).equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
updateAiTaskFail(task, error);
|
||||
}
|
||||
|
||||
private AiTask findLatestSummaryTask(Long meetingId) {
|
||||
return this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, "SUMMARY")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
private void processSummaryTask(Meeting meeting, String asrText, AiTask taskRecord) throws Exception {
|
||||
updateMeetingStatus(meeting.getId(), 2);
|
||||
updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
|
|
@ -10,12 +13,23 @@ import com.imeeting.support.TaskSecurityContextRunner;
|
|||
import com.unisbase.mapper.SysUserMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AiTaskServiceImplTest {
|
||||
|
||||
|
|
@ -60,18 +74,118 @@ class AiTaskServiceImplTest {
|
|||
assertEquals("http://10.100.52.43:1234/v1/chat/completions", uri.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatchTasksShouldFailSummaryTaskWhenTranscriptContentIsBlank() {
|
||||
MeetingMapper meetingMapper = mock(MeetingMapper.class);
|
||||
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any())).thenReturn(true);
|
||||
|
||||
AiTaskServiceImpl service = spy(createService(
|
||||
meetingMapper,
|
||||
transcriptMapper,
|
||||
aiModelService,
|
||||
redisTemplate,
|
||||
new TaskSecurityContextRunner()
|
||||
));
|
||||
doReturn(true).when(service).updateById(any());
|
||||
|
||||
Meeting meeting = new Meeting();
|
||||
meeting.setId(66L);
|
||||
meeting.setAudioUrl("/audio/demo.wav");
|
||||
when(meetingMapper.selectById(66L)).thenReturn(meeting);
|
||||
|
||||
MeetingTranscript transcript = new MeetingTranscript();
|
||||
transcript.setSpeakerName("Alice");
|
||||
transcript.setContent(" ");
|
||||
when(transcriptMapper.selectList(any())).thenReturn(List.of(transcript));
|
||||
|
||||
AiTask summaryTask = new AiTask();
|
||||
summaryTask.setId(99L);
|
||||
summaryTask.setMeetingId(66L);
|
||||
summaryTask.setTaskType("SUMMARY");
|
||||
summaryTask.setStatus(0);
|
||||
doReturn(null, summaryTask).when(service).getOne(any());
|
||||
|
||||
service.dispatchTasks(66L, 1L, 2L);
|
||||
|
||||
assertEquals(3, summaryTask.getStatus());
|
||||
assertTrue(summaryTask.getErrorMsg().contains("No transcript content available for summary"));
|
||||
verify(aiModelService, never()).getModelById(anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatchSummaryTaskShouldFailWhenTranscriptContentIsBlank() {
|
||||
MeetingMapper meetingMapper = mock(MeetingMapper.class);
|
||||
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
|
||||
AiModelService aiModelService = mock(AiModelService.class);
|
||||
StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ValueOperations<String, String> valueOperations = mock(ValueOperations.class);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||
|
||||
AiTaskServiceImpl service = spy(createService(
|
||||
meetingMapper,
|
||||
transcriptMapper,
|
||||
aiModelService,
|
||||
redisTemplate,
|
||||
new TaskSecurityContextRunner()
|
||||
));
|
||||
doReturn(true).when(service).updateById(any());
|
||||
|
||||
Meeting meeting = new Meeting();
|
||||
meeting.setId(77L);
|
||||
when(meetingMapper.selectById(77L)).thenReturn(meeting);
|
||||
|
||||
MeetingTranscript transcript = new MeetingTranscript();
|
||||
transcript.setSpeakerId("spk-1");
|
||||
transcript.setContent(" ");
|
||||
when(transcriptMapper.selectList(any())).thenReturn(List.of(transcript));
|
||||
|
||||
AiTask summaryTask = new AiTask();
|
||||
summaryTask.setId(100L);
|
||||
summaryTask.setMeetingId(77L);
|
||||
summaryTask.setTaskType("SUMMARY");
|
||||
summaryTask.setStatus(0);
|
||||
doReturn(summaryTask).when(service).getOne(any());
|
||||
|
||||
service.dispatchSummaryTask(77L, 1L, 2L);
|
||||
|
||||
assertEquals(3, summaryTask.getStatus());
|
||||
assertTrue(summaryTask.getErrorMsg().contains("No transcript content available for summary"));
|
||||
verify(aiModelService, never()).getModelById(anyLong(), anyString());
|
||||
}
|
||||
|
||||
private AiTaskServiceImpl createService() {
|
||||
return new AiTaskServiceImpl(
|
||||
return createService(
|
||||
mock(MeetingMapper.class),
|
||||
mock(MeetingTranscriptMapper.class),
|
||||
mock(AiModelService.class),
|
||||
new ObjectMapper(),
|
||||
mock(SysUserMapper.class),
|
||||
mock(HotWordService.class),
|
||||
mock(StringRedisTemplate.class),
|
||||
mock(MeetingSummaryFileService.class),
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
mock(TaskSecurityContextRunner.class)
|
||||
);
|
||||
}
|
||||
|
||||
private AiTaskServiceImpl createService(MeetingMapper meetingMapper,
|
||||
MeetingTranscriptMapper transcriptMapper,
|
||||
AiModelService aiModelService,
|
||||
StringRedisTemplate redisTemplate,
|
||||
TaskSecurityContextRunner taskSecurityContextRunner) {
|
||||
return new AiTaskServiceImpl(
|
||||
meetingMapper,
|
||||
transcriptMapper,
|
||||
aiModelService,
|
||||
new ObjectMapper(),
|
||||
mock(SysUserMapper.class),
|
||||
mock(HotWordService.class),
|
||||
redisTemplate,
|
||||
mock(MeetingSummaryFileService.class),
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
taskSecurityContextRunner
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,38 @@ import {SysUser} from '../../types';
|
|||
const { Text, Title } = Typography;
|
||||
const { Option } = Select;
|
||||
const PAUSED_DISPLAY_STATUS = 5;
|
||||
const REALTIME_ACTIVE_DISPLAY_STATUS = 6;
|
||||
const REALTIME_IDLE_DISPLAY_STATUS = 7;
|
||||
|
||||
const isRealtimeMeetingCandidate = (item: MeetingVO) => item.status === 0 && !item.audioUrl;
|
||||
|
||||
const isPausedRealtimeSessionStatus = (status?: RealtimeMeetingSessionStatus["status"]) =>
|
||||
status === 'PAUSED_EMPTY' || status === 'PAUSED_RESUMABLE';
|
||||
|
||||
const canOpenRealtimeSession = (status?: RealtimeMeetingSessionStatus["status"]) =>
|
||||
status === 'PAUSED_EMPTY'
|
||||
|| status === 'PAUSED_RESUMABLE'
|
||||
|| status === 'ACTIVE'
|
||||
|| status === 'IDLE';
|
||||
|
||||
const shouldPollMeetingCard = (item: MeetingVO) =>
|
||||
item.status === 1
|
||||
|| item.status === 2
|
||||
|| item.realtimeSessionStatus === 'ACTIVE'
|
||||
|| isPausedRealtimeSessionStatus(item.realtimeSessionStatus);
|
||||
|
||||
const canManageRealtimeMeeting = (meeting: MeetingVO) => {
|
||||
try {
|
||||
const profileStr = sessionStorage.getItem('userProfile');
|
||||
if (!profileStr) {
|
||||
return false;
|
||||
}
|
||||
const profile = JSON.parse(profileStr);
|
||||
return profile.isPlatformAdmin === true || profile.isTenantAdmin === true || profile.userId === meeting.creatorId;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const applyRealtimeSessionStatus = (item: MeetingVO, sessionStatus?: RealtimeMeetingSessionStatus): MeetingVO => {
|
||||
if (!sessionStatus) {
|
||||
|
|
@ -72,14 +104,14 @@ const applyRealtimeSessionStatus = (item: MeetingVO, sessionStatus?: RealtimeMee
|
|||
if (sessionStatus.status === 'ACTIVE') {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: 1,
|
||||
displayStatus: REALTIME_ACTIVE_DISPLAY_STATUS,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
if (sessionStatus.status === 'IDLE' && !item.audioUrl) {
|
||||
if (sessionStatus.status === 'IDLE' && isRealtimeMeetingCandidate(item)) {
|
||||
return {
|
||||
...item,
|
||||
displayStatus: 0,
|
||||
displayStatus: REALTIME_IDLE_DISPLAY_STATUS,
|
||||
realtimeSessionStatus: sessionStatus.status
|
||||
};
|
||||
}
|
||||
|
|
@ -93,8 +125,7 @@ const applyRealtimeSessionStatus = (item: MeetingVO, sessionStatus?: RealtimeMee
|
|||
const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => {
|
||||
const [progress, setProgress] = useState<MeetingProgress | null>(null);
|
||||
useEffect(() => {
|
||||
const effectiveStatus = meeting.displayStatus ?? meeting.status;
|
||||
if (effectiveStatus !== 1 && effectiveStatus !== 2) return;
|
||||
if (meeting.status !== 1 && meeting.status !== 2) return;
|
||||
const fetchProgress = async () => {
|
||||
try {
|
||||
const res = await getMeetingProgress(meeting.id);
|
||||
|
|
@ -109,7 +140,7 @@ const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => {
|
|||
fetchProgress();
|
||||
const timer = setInterval(fetchProgress, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [meeting.id, meeting.status, meeting.displayStatus]);
|
||||
}, [meeting.id, meeting.status]);
|
||||
return progress;
|
||||
};
|
||||
|
||||
|
|
@ -122,11 +153,13 @@ const IntegratedStatusTag: React.FC<{ meeting: MeetingVO, progress: MeetingProgr
|
|||
2: { text: '总结中', color: '#faad14', bgColor: '#fff7e6' },
|
||||
3: { text: '已完成', color: '#52c41a', bgColor: '#f6ffed' },
|
||||
4: { text: '失败', color: '#ff4d4f', bgColor: '#fff1f0' },
|
||||
5: { text: '会议暂停', color: '#d48806', bgColor: '#fff7e6' }
|
||||
5: { text: '会议暂停', color: '#d48806', bgColor: '#fff7e6' },
|
||||
6: { text: '实时进行中', color: '#1677ff', bgColor: '#e6f4ff' },
|
||||
7: { text: '待开始', color: '#595959', bgColor: '#f5f5f5' }
|
||||
};
|
||||
const config = statusConfig[effectiveStatus] || statusConfig[0];
|
||||
const percent = progress?.percent || 0;
|
||||
const isProcessing = effectiveStatus === 1 || effectiveStatus === 2;
|
||||
const percent = (meeting.status === 1 || meeting.status === 2) ? (progress?.percent || 0) : 0;
|
||||
const isProcessing = meeting.status === 1 || meeting.status === 2;
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', padding: '2px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600, color: config.color, background: config.bgColor, position: 'relative', overflow: 'hidden', border: `1px solid ${isProcessing ? 'transparent' : '#eee'}`, minWidth: 80, justifyContent: 'center' }}>
|
||||
{isProcessing && percent > 0 && (
|
||||
|
|
@ -151,8 +184,10 @@ const TableStatusCell: React.FC<{ meeting: MeetingVO, fetchData: () => void }> =
|
|||
const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () => void, t: any, onEditParticipants: (meeting: MeetingVO) => void, onOpenMeeting: (meeting: MeetingVO) => void }> = ({ item, config, fetchData, t, onEditParticipants, onOpenMeeting }) => {
|
||||
const progress = useMeetingProgress(item, () => fetchData());
|
||||
const effectiveStatus = item.displayStatus ?? item.status;
|
||||
const isProcessing = effectiveStatus === 1 || effectiveStatus === 2;
|
||||
const isProcessing = item.status === 1 || item.status === 2;
|
||||
const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS;
|
||||
const isRealtimeActive = effectiveStatus === REALTIME_ACTIVE_DISPLAY_STATUS;
|
||||
const isRealtimeIdle = effectiveStatus === REALTIME_IDLE_DISPLAY_STATUS;
|
||||
|
||||
return (
|
||||
<List.Item>
|
||||
|
|
@ -164,7 +199,7 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
|
|||
<Tooltip title="编辑参会人"><div className="icon-btn edit"><EditOutlined onClick={() => onEditParticipants(item)} /></div></Tooltip>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={(e) => { e?.stopPropagation(); deleteMeeting(item.id).then(fetchData); }}
|
||||
onConfirm={(e) => { e?.stopPropagation(); deleteMeeting(item.id).then(() => fetchData()); }}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
|
|
@ -231,6 +266,44 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
|
|||
会议已暂停,可继续识别
|
||||
</Text>
|
||||
</div>
|
||||
) : isRealtimeActive ? (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#1677ff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: '#e6f4ff',
|
||||
padding: '6px 10px',
|
||||
borderRadius: 6,
|
||||
marginTop: 4,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<SyncOutlined spin style={{ marginRight: 6, flexShrink: 0 }} />
|
||||
<Text ellipsis style={{ color: 'inherit', fontSize: '12px', fontWeight: 500, flex: 1, minWidth: 0 }}>
|
||||
实时会议进行中,可直接进入控制页
|
||||
</Text>
|
||||
</div>
|
||||
) : isRealtimeIdle ? (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#595959',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: '#f5f5f5',
|
||||
padding: '6px 10px',
|
||||
borderRadius: 6,
|
||||
marginTop: 4,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<InfoCircleOutlined style={{ marginRight: 6, flexShrink: 0 }} />
|
||||
<Text ellipsis style={{ color: 'inherit', fontSize: '12px', fontWeight: 500, flex: 1, minWidth: 0 }}>
|
||||
实时会议尚未开始,可进入继续操作
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
<div style={{ fontSize: '13px', color: '#8c8c8c', display: 'flex', alignItems: 'center' }}><TeamOutlined style={{ marginRight: 10, flexShrink: 0 }} /><Text type="secondary" ellipsis style={{ flex: 1, minWidth: 0 }}>{item.participants || '无参与人员'}</Text></div>
|
||||
|
|
@ -271,10 +344,7 @@ const Meetings: React.FC = () => {
|
|||
const [editingMeeting, setEditingMeeting] = useState<MeetingVO | null>(null);
|
||||
const [participantsEditLoading, setParticipantsEditLoading] = useState(false);
|
||||
const [participantsEditForm] = Form.useForm();
|
||||
const hasRunningTasks = data.some(item => {
|
||||
const effectiveStatus = item.displayStatus ?? item.status;
|
||||
return effectiveStatus === 0 || effectiveStatus === 1 || effectiveStatus === 2;
|
||||
});
|
||||
const hasRunningTasks = data.some((item) => shouldPollMeetingCard(item));
|
||||
|
||||
const handleDisplayModeChange = (mode: 'card' | 'list') => {
|
||||
setDisplayMode(mode);
|
||||
|
|
@ -309,10 +379,13 @@ const Meetings: React.FC = () => {
|
|||
if (res.data && res.data.data) {
|
||||
const records = res.data.data.records || [];
|
||||
let statusMap: Record<number, RealtimeMeetingSessionStatus> = {};
|
||||
try {
|
||||
const sessionRes = await getRealtimeMeetingSessionStatuses(records.map((item) => item.id));
|
||||
statusMap = sessionRes.data?.data || {};
|
||||
} catch {}
|
||||
const realtimeCandidates = records.filter(isRealtimeMeetingCandidate);
|
||||
if (realtimeCandidates.length > 0) {
|
||||
try {
|
||||
const sessionRes = await getRealtimeMeetingSessionStatuses(realtimeCandidates.map((item) => item.id));
|
||||
statusMap = sessionRes.data?.data || {};
|
||||
} catch {}
|
||||
}
|
||||
const withDisplayStatus = records.map((item) => applyRealtimeSessionStatus(item, statusMap[item.id]));
|
||||
setData(withDisplayStatus);
|
||||
setTotal(res.data.data.total);
|
||||
|
|
@ -321,15 +394,25 @@ const Meetings: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleOpenMeeting = async (meeting: MeetingVO) => {
|
||||
if (!isRealtimeMeetingCandidate(meeting)) {
|
||||
navigate(`/meetings/${meeting.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canOpenRealtimeSession(meeting.realtimeSessionStatus)) {
|
||||
navigate(`/meeting-live-session/${meeting.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canManageRealtimeMeeting(meeting)) {
|
||||
navigate(`/meetings/${meeting.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getRealtimeMeetingSessionStatus(meeting.id);
|
||||
const sessionStatus = res.data?.data;
|
||||
if (sessionStatus && !meeting.audioUrl && (
|
||||
sessionStatus.status === 'PAUSED_EMPTY'
|
||||
|| sessionStatus.status === 'PAUSED_RESUMABLE'
|
||||
|| sessionStatus.status === 'ACTIVE'
|
||||
|| sessionStatus.status === 'IDLE'
|
||||
)) {
|
||||
const sessionStatus = res.data?.data?.status;
|
||||
if (canOpenRealtimeSession(sessionStatus)) {
|
||||
navigate(`/meeting-live-session/${meeting.id}`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -371,7 +454,9 @@ const Meetings: React.FC = () => {
|
|||
2: { text: '总结中', color: '#faad14', bgColor: '#fff7e6' },
|
||||
3: { text: '已完成', color: '#52c41a', bgColor: '#f6ffed' },
|
||||
4: { text: '失败', color: '#ff4d4f', bgColor: '#fff1f0' },
|
||||
5: { text: '会议暂停', color: '#d48806', bgColor: '#fff7e6' }
|
||||
5: { text: '会议暂停', color: '#d48806', bgColor: '#fff7e6' },
|
||||
6: { text: '实时进行中', color: '#1677ff', bgColor: '#e6f4ff' },
|
||||
7: { text: '待开始', color: '#595959', bgColor: '#f5f5f5' }
|
||||
};
|
||||
|
||||
const tableColumns = [
|
||||
|
|
@ -415,7 +500,7 @@ const Meetings: React.FC = () => {
|
|||
<Button type="link" size="small" onClick={(e) => { e.stopPropagation(); openEditParticipants(record); }}>编辑</Button>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={(e) => { e?.stopPropagation(); deleteMeeting(record.id).then(fetchData); }}
|
||||
onConfirm={(e) => { e?.stopPropagation(); deleteMeeting(record.id).then(() => fetchData()); }}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
|
|
|
|||
Loading…
Reference in New Issue