refactor: 优化MeetingDetail页面和后端时间处理逻辑
- 优化 `MeetingDetail` 页面的待办事项和侧边栏样式 - 更新 `AiTaskServiceImpl` 和 `MeetingSummaryFileServiceImpl`dev_na
parent
12c79cdf26
commit
8dbed4c8e6
|
|
@ -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。",
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue