feat: 优化会议创建表单和卡片显示

- 重构会议创建表单,增加录音上传、AI分析配置和参会人员选择
- 更新会议卡片组件,集成进度背景和状态标签,优化样式和交互体验
- 增加分页功能和多语言支持
- 修复和优化多处代码逻辑和样式问题
dev_na
chenhao 2026-03-04 20:59:49 +08:00
parent 423327c61d
commit 11ab76f2ed
3 changed files with 29 additions and 15 deletions

View File

@ -84,7 +84,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
} catch (Exception e) { } catch (Exception e) {
log.error("Meeting {} AI Task Flow failed", meetingId, e); log.error("Meeting {} AI Task Flow failed", meetingId, e);
updateMeetingStatus(meetingId, 4); // Overall Failed updateMeetingStatus(meetingId, 4); // Overall Failed
updateProgress(meetingId, -1, "分析失败: " + e.getMessage()); updateProgress(meetingId, -1, "分析失败: " + e.getMessage(), 0);
} finally { } finally {
redisTemplate.delete(lockKey); redisTemplate.delete(lockKey);
} }
@ -115,7 +115,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
private String processAsrTask(Meeting meeting) throws Exception { private String processAsrTask(Meeting meeting) throws Exception {
updateMeetingStatus(meeting.getId(), 1); // 识别中 updateMeetingStatus(meeting.getId(), 1); // 识别中
updateProgress(meeting.getId(), 5, "已提交识别请求..."); updateProgress(meeting.getId(), 5, "已提交识别请求...", 0);
AiModel asrModel = aiModelService.getById(meeting.getAsrModelId()); AiModel asrModel = aiModelService.getById(meeting.getAsrModelId());
if (asrModel == null) throw new RuntimeException("ASR Model config not found"); if (asrModel == null) throw new RuntimeException("ASR Model config not found");
@ -177,7 +177,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
if ("completed".equalsIgnoreCase(status)) { if ("completed".equalsIgnoreCase(status)) {
resultNode = data.path("result"); resultNode = data.path("result");
updateAiTaskSuccess(taskRecord, statusNode); updateAiTaskSuccess(taskRecord, statusNode);
updateProgress(meeting.getId(), 85, "语音转录完成,准备进行总结..."); updateProgress(meeting.getId(), 85, "语音转录完成,准备进行总结...", 0);
break; break;
} else if ("failed".equalsIgnoreCase(status)) { } else if ("failed".equalsIgnoreCase(status)) {
updateAiTaskFail(taskRecord, "ASR reported failure: " + queryResp); updateAiTaskFail(taskRecord, "ASR reported failure: " + queryResp);
@ -186,10 +186,11 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
// 处理中:同步进度到 Redis // 处理中:同步进度到 Redis
int currentPercent = data.path("percentage").asInt(); int currentPercent = data.path("percentage").asInt();
String message = data.path("message").asText(); String message = data.path("message").asText();
int eta = data.path("eta").asInt(0);
// 缩放到 0-85% 范围 // 缩放到 0-85% 范围
int scaledPercent = (int)(currentPercent * 0.85); int scaledPercent = (int)(currentPercent * 0.85);
updateProgress(meeting.getId(), Math.max(5, scaledPercent), message); updateProgress(meeting.getId(), Math.max(5, scaledPercent), message, eta);
// 防死循环逻辑:如果进度长时间不动且不是 0 // 防死循环逻辑:如果进度长时间不动且不是 0
if (currentPercent > 0 && currentPercent == lastPercent) { if (currentPercent > 0 && currentPercent == lastPercent) {
@ -241,7 +242,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
private void processSummaryTask(Meeting meeting, String asrText) throws Exception { private void processSummaryTask(Meeting meeting, String asrText) throws Exception {
updateMeetingStatus(meeting.getId(), 2); // 总结中 updateMeetingStatus(meeting.getId(), 2); // 总结中
updateProgress(meeting.getId(), 90, "正在进行 AI 智能总结..."); updateProgress(meeting.getId(), 90, "正在进行 AI 智能总结...", 0);
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId()); AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
if (llmModel == null) return; if (llmModel == null) return;
@ -273,18 +274,19 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
meeting.setStatus(3); // Finished meeting.setStatus(3); // Finished
meetingMapper.updateById(meeting); meetingMapper.updateById(meeting);
updateAiTaskSuccess(taskRecord, respNode); updateAiTaskSuccess(taskRecord, respNode);
updateProgress(meeting.getId(), 100, "分析已完成"); updateProgress(meeting.getId(), 100, "分析已完成", 0);
} else { } else {
updateAiTaskFail(taskRecord, "LLM failed: " + response.body()); updateAiTaskFail(taskRecord, "LLM failed: " + response.body());
throw new RuntimeException("AI总结生成失败"); throw new RuntimeException("AI总结生成失败");
} }
} }
private void updateProgress(Long meetingId, int percent, String msg) { private void updateProgress(Long meetingId, int percent, String msg, int eta) {
try { try {
Map<String, Object> progress = new HashMap<>(); Map<String, Object> progress = new HashMap<>();
progress.put("percent", percent); progress.put("percent", percent);
progress.put("message", msg); progress.put("message", msg);
progress.put("eta", eta);
progress.put("updateAt", System.currentTimeMillis()); progress.put("updateAt", System.currentTimeMillis());
redisTemplate.opsForValue().set(RedisKeys.meetingProgressKey(meetingId), redisTemplate.opsForValue().set(RedisKeys.meetingProgressKey(meetingId),
objectMapper.writeValueAsString(progress), 1, TimeUnit.HOURS); objectMapper.writeValueAsString(progress), 1, TimeUnit.HOURS);

View File

@ -39,6 +39,15 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
const percent = progress?.percent || 0; const percent = progress?.percent || 0;
const isError = percent < 0; const isError = percent < 0;
// 格式化剩余时间 (ETA)
const formatETA = (seconds?: number) => {
if (!seconds || seconds <= 0) return '即将完成';
if (seconds < 60) return `${seconds}`;
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return s > 0 ? `${m}${s}` : `${m}分钟`;
};
return ( return (
<div style={{ <div style={{
height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center',
@ -71,7 +80,7 @@ const MeetingProgressDisplay: React.FC<{ meetingId: number; onComplete: () => vo
<Col span={8}> <Col span={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary" size="small"></Text> <Text type="secondary" size="small"></Text>
<Title level={4} style={{ margin: 0 }}>{isError ? '--' : (percent > 90 ? '即将完成' : '计算中')}</Title> <Title level={4} style={{ margin: 0 }}>{isError ? '--' : formatETA(progress?.eta)}</Title>
</Space> </Space>
</Col> </Col>
<Col span={8}> <Col span={8}>

View File

@ -293,22 +293,25 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
background: item.status === 1 ? '#e6f7ff' : '#fff7e6', background: item.status === 1 ? '#e6f7ff' : '#fff7e6',
padding: '6px 12px', // 增加内边距,更聚拢 padding: '6px 10px',
borderRadius: 6, borderRadius: 6,
marginTop: 4, marginTop: 4,
width: 'calc(100% - 12px)', // 留出右侧与卡片边缘的距离 width: '100%', // 占满 Space 容器
overflow: 'hidden', overflow: 'hidden',
boxSizing: 'border-box' boxSizing: 'border-box',
minWidth: 0 // 关键:允许 flex 子项收缩
}}> }}>
<InfoCircleOutlined style={{ marginRight: 8, flexShrink: 0 }} /> <InfoCircleOutlined style={{ marginRight: 6, flexShrink: 0 }} />
<Text <Text
ellipsis={{ tooltip: progress?.message || '处理中...' }} ellipsis={{ tooltip: progress?.message || '分析中...' }}
style={{ style={{
color: 'inherit', color: 'inherit',
fontSize: '12px', fontSize: '12px',
flex: 1, flex: 1,
minWidth: 0, minWidth: 0, // 关键:触发文本截断
fontWeight: 500 maxWidth: 250, // 关键:触发文本截断
fontWeight: 500,
whiteSpace: 'nowrap'
}} }}
> >
{progress?.message || '等待引擎调度...'} {progress?.message || '等待引擎调度...'}