refactor: 优化MeetingDetail页面和后端时间处理逻辑

- 优化 `MeetingDetail` 页面的待办事项和侧边栏样式
- 更新 `AiTaskServiceImpl` 和 `MeetingSummaryFileServiceImpl`
dev_na
chenhao 2026-03-27 10:52:01 +08:00
parent 12c79cdf26
commit 8dbed4c8e6
3 changed files with 107 additions and 11 deletions

View File

@ -588,7 +588,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
" \"keywords\": [\"关键词1\", \"关键词2\"],",
" \"chapters\": [{\"time\":\"00:00\",\"title\":\"章节标题\",\"summary\":\"章节摘要\"}],",
" \"speakerSummaries\": [{\"speaker\":\"发言人 1\",\"summary\":\"该发言人在整场会议中的主要观点总结\"}],",
" \"keyPoints\": [{\"title\":\"重点问题或结论\",\"summary\":\"具体说明\",\"speaker\":\"发言人 1\",\"time\":\"00:00\"}],",
" \"keyPoints\": [{\"title\":\"重点问题或结论\",\"summary\":\"具体说明\",\"speaker\":\"发言人 1\"}],",
" \"todos\": [\"待办事项1\", \"待办事项2\"]",
" }",
"}",
@ -599,7 +599,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
"4. analysis 是给页面结构化展示使用的单独结果。",
"5. analysis.overview 必须基于所有会话内容,控制在 300 字内。",
"6. analysis.speakerSummaries 必须按发言人聚合,每个发言人只出现一次。",
"7. analysis.chapters 按时间顺序输出time 使用 mm:ss 或 hh:mm:ss。",
"7. analysis.chapters 按时间顺序输出time 使用 mm:ss 或 hh:mm:ss。time需要与讨论内容相对应",
"8. analysis.keyPoints 提炼关键问题、决定、结论或争议点。",
"9. analysis.todos 尽量写成可执行动作;没有就返回空数组。",
"10. 只返回 JSON。",

View File

@ -349,7 +349,7 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService
continue;
}
Map<String, Object> normalized = new LinkedHashMap<>();
normalized.put("time", asText(item.get("time")));
normalized.put("time", normalizeTimeText(item.get("time")));
normalized.put("title", title);
normalized.put("summary", summary);
result.add(normalized);
@ -385,7 +385,7 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService
normalized.put("title", title);
normalized.put("summary", summary);
normalized.put("speaker", asText(item.get("speaker")));
normalized.put("time", asText(item.get("time")));
normalized.put("time", normalizeTimeText(item.get("time")));
result.add(normalized);
}
return result;
@ -407,6 +407,38 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService
return value == null ? "" : String.valueOf(value).trim();
}
private String normalizeTimeText(Object value) {
String text = asText(value);
if (text.isBlank()) {
return "";
}
if (text.contains(":")) {
return text;
}
if (!text.matches("^\\d+(\\.\\d+)?$")) {
return text;
}
double numeric = Double.parseDouble(text);
long totalSeconds;
if (numeric >= 1000) {
totalSeconds = Math.round(numeric / 1000D);
} else {
totalSeconds = Math.round(numeric);
}
if (totalSeconds < 0) {
totalSeconds = 0;
}
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
if (hours > 0) {
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
return String.format("%02d:%02d", minutes, seconds);
}
private String clipText(String text, int limit) {
if (text == null) {
return "";

View File

@ -914,10 +914,10 @@ const MeetingDetail: React.FC = () => {
<div className="todo-list">
{analysis.todos.length ? (
analysis.todos.map((item, index) => (
<label className="todo-item" key={`${item}-${index}`}>
<input type="checkbox" readOnly />
<div className="todo-item" key={`${item}-${index}`}>
<span className="todo-dot" />
<span>{item}</span>
</label>
</div>
))
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无待办事项" />
@ -1011,7 +1011,7 @@ const MeetingDetail: React.FC = () => {
</Col>
<Col xs={24} lg={10} style={{ height: '100%' }}>
<div className="detail-side-column">
<div className="detail-side-column ai-summary-column">
<Card
className="left-flow-card"
bordered={false}
@ -1041,7 +1041,7 @@ const MeetingDetail: React.FC = () => {
)
}
style={{ height: '100%' }}
bodyStyle={{ padding: 24, height: '100%', overflowY: 'auto' }}
bodyStyle={{ padding: 24, height: '100%', overflowY: 'auto', overflowX: 'hidden', minWidth: 0 }}
>
<div ref={summaryPdfRef} className="markdown-shell">
{meeting.summaryContent ? (
@ -1080,20 +1080,31 @@ const MeetingDetail: React.FC = () => {
.detail-side-column {
height: 100%;
min-height: 0;
min-width: 0;
}
.detail-left-column {
overflow-y: auto;
overflow-x: hidden;
padding-right: 6px;
display: flex;
flex-direction: column;
gap: 16px;
}
.left-flow-card {
min-width: 0;
border-radius: 22px;
border: 1px solid rgba(228, 232, 245, 0.96);
box-shadow: 0 16px 34px rgba(127, 139, 186, 0.08);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 255, 0.98));
}
.ai-summary-column,
.ai-summary-column .left-flow-card {
overflow: hidden;
}
.left-flow-card .ant-card-head,
.left-flow-card .ant-card-body {
min-width: 0;
}
.summary-panel .ant-card-body {
display: flex;
flex-direction: column;
@ -1360,8 +1371,13 @@ const MeetingDetail: React.FC = () => {
background: rgba(247, 249, 255, 0.98);
border: 1px solid rgba(232, 236, 248, 0.98);
}
.todo-item input {
margin-top: 4px;
.todo-dot {
width: 8px;
height: 8px;
margin-top: 9px;
border-radius: 50%;
background: #6e76ff;
flex: 0 0 auto;
}
.section-divider-note {
display: flex;
@ -1526,16 +1542,33 @@ const MeetingDetail: React.FC = () => {
.markdown-shell {
height: 100%;
min-height: 0;
min-width: 0;
width: 100%;
overflow-x: hidden;
padding-bottom: 28px;
box-sizing: border-box;
}
.summary-markdown {
font-size: 14px;
line-height: 1.9;
color: #3e4768;
min-width: 0;
width: 100%;
max-width: 100%;
overflow-x: hidden;
padding-bottom: 12px;
box-sizing: border-box;
}
.markdown-body {
font-size: 14px;
line-height: 1.8;
color: #333;
min-width: 0;
width: 100%;
box-sizing: border-box;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.markdown-body p {
margin-bottom: 16px;
@ -1546,6 +1579,37 @@ const MeetingDetail: React.FC = () => {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
overflow-wrap: anywhere;
word-break: break-word;
}
.markdown-body ul,
.markdown-body ol,
.markdown-body li,
.markdown-body blockquote,
.markdown-body table,
.markdown-body tr,
.markdown-body td,
.markdown-body th,
.markdown-body a,
.markdown-body code,
.markdown-body pre {
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.markdown-body pre {
overflow-x: auto;
padding: 12px 14px;
border-radius: 12px;
background: rgba(245, 247, 255, 0.96);
}
.markdown-body table {
display: block;
overflow-x: auto;
}
.markdown-body img {
max-width: 100%;
height: auto;
}
@media (max-width: 1200px) {
.detail-left-column {