feat(aitutor): 新增聊天历史分页加载和Markdown渲染功能

为聊天历史页面添加分页加载功能,支持加载更多历史消息
使用marked库实现AI回答的Markdown格式渲染
优化聊天历史对话框的样式和交互体验
This commit is contained in:
2025-08-14 10:13:53 +08:00
parent 3c451db37c
commit 1268c2fd9c
2 changed files with 297 additions and 4 deletions

View File

@@ -63,6 +63,7 @@
"jspdf": "^2.5.2",
"lodash": "^4.17.21",
"mapv-three": "^1.0.18",
"marked": "^4.3.0",
"nprogress": "0.2.0",
"print-js": "^1.6.0",
"quill": "1.3.7",

View File

@@ -135,10 +135,30 @@
<div class="message-text ai-text" :class="{
'feedback-like': message.feedback && message.feedback.rating === 'like',
'feedback-dislike': message.feedback && message.feedback.rating === 'dislike'
}">{{ message.answer }}</div>
}" v-html="renderMarkdown(message.answer)"></div>
</div>
</div>
</div>
<!-- 加载更多按钮 -->
<div class="load-more-container" v-if="chatMessages.length > 0">
<el-button
v-if="hasMoreMessages && !isLoadingMore"
type="primary"
size="small"
@click="loadMoreMessages"
:loading="loadMoreLoading"
class="load-more-btn"
>
加载更多历史消息
</el-button>
<div v-if="!hasMoreMessages" class="no-more-messages">
<el-divider>
<span class="no-more-text">已经到底了</span>
</el-divider>
</div>
</div>
</div>
</el-dialog>
</div>
@@ -149,6 +169,7 @@ import { listStudent, getClassName } from "@/api/stuCQS/basedata/student";
import { getMessagesToAdmin } from "@/api/aitutor/chat";
import { listGrade } from "@/api/stuCQS/basedata/grade";
import { getDeptName } from "@/api/system/dept";
import { marked } from 'marked';
export default {
name: "ChatHistory",
@@ -186,7 +207,11 @@ export default {
dialogVisible: false,
chatMessages: [],
chatLoading: false,
currentStudent: null
currentStudent: null,
// 分页加载相关
loadMoreLoading: false,
hasMoreMessages: true,
isLoadingMore: false
};
},
created() {
@@ -264,13 +289,24 @@ export default {
this.chatLoading = true;
this.chatMessages = [];
// 重置分页状态
this.hasMoreMessages = true;
this.isLoadingMore = false;
this.loadMoreLoading = false;
getMessagesToAdmin({
user: row.stuNo
user: row.stuNo,
limit: 20
}).then(response => {
console.log('对话记录API响应:', response);
if (response.code === 200 && response.data && response.data.data) {
this.chatMessages = response.data.data;
// 按照created_at时间戳进行降序排序最新的消息在最上面
this.chatMessages = response.data.data.sort((a, b) => b.created_at - a.created_at);
// 如果返回的消息数量少于20条说明没有更多消息了
if (response.data.data.length < 20) {
this.hasMoreMessages = false;
}
} else {
this.$modal.msgWarning(response.msg || '该学生暂无对话记录');
}
@@ -287,6 +323,95 @@ export default {
this.dialogVisible = false;
this.currentStudent = null;
this.chatMessages = [];
// 重置分页状态
this.hasMoreMessages = true;
this.isLoadingMore = false;
this.loadMoreLoading = false;
},
/** 加载更多历史消息 */
loadMoreMessages() {
if (this.loadMoreLoading || !this.hasMoreMessages || this.chatMessages.length === 0) {
return;
}
this.loadMoreLoading = true;
this.isLoadingMore = true;
// 获取当前消息列表中最早的消息IDcreated_at最小的
const earliestMessage = this.chatMessages.reduce((earliest, current) => {
return current.created_at < earliest.created_at ? current : earliest;
});
getMessagesToAdmin({
user: this.currentStudent.stuNo,
limit: 50,
firstId: earliestMessage.id
}).then(response => {
console.log('加载更多消息API响应:', response);
if (response.code === 200 && response.data && response.data.data) {
const newMessages = response.data.data;
if (newMessages.length === 0) {
// 没有更多消息了
this.hasMoreMessages = false;
this.$message.info('已经到底了');
} else {
// 将新消息按时间排序后添加到现有消息列表的底部
const sortedNewMessages = newMessages.sort((a, b) => b.created_at - a.created_at);
this.chatMessages = [...this.chatMessages, ...sortedNewMessages];
// 如果返回的消息数量少于50条说明没有更多消息了
if (newMessages.length < 50) {
this.hasMoreMessages = false;
}
}
} else {
this.$message.error('加载更多消息失败');
}
this.loadMoreLoading = false;
this.isLoadingMore = false;
}).catch(error => {
console.error('加载更多消息失败:', error);
this.$message.error('加载更多消息失败');
this.loadMoreLoading = false;
this.isLoadingMore = false;
});
},
/** 渲染Markdown内容 */
renderMarkdown(content) {
if (!content) return '';
// 配置marked选项
marked.setOptions({
breaks: true, // 支持换行
gfm: true, // 支持GitHub风格的markdown
sanitize: false // 允许HTML标签
});
// 自定义渲染器,为链接添加新标签页打开属性和内联样式
const renderer = new marked.Renderer();
renderer.link = function(href, title, text) {
const titleAttr = title ? ` title="${title}"` : '';
// 直接使用内联样式来确保样式生效绕过CSS优先级问题
const inlineStyle = `
color: #1890ff !important;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(64, 158, 255, 0.1) 100%) !important;
border: 1px solid rgba(24, 144, 255, 0.3) !important;
border-radius: 4px !important;
padding: 2px 6px !important;
text-decoration: none !important;
display: inline-block !important;
margin: 0 2px !important;
transition: all 0.3s ease !important;
position: relative !important;
font-weight: 500 !important;
`;
return `<a href="${href}"${titleAttr} target="_blank" rel="noopener noreferrer" class="markdown-link" style="${inlineStyle}" onmouseover="this.style.background='linear-gradient(135deg, rgba(24, 144, 255, 0.2) 0%, rgba(64, 158, 255, 0.2) 100%)'; this.style.boxShadow='0 2px 8px rgba(24, 144, 255, 0.3)'; this.style.transform='translateY(-1px)';" onmouseout="this.style.background='linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(64, 158, 255, 0.1) 100%)'; this.style.boxShadow='none'; this.style.transform='translateY(0)';">${text}</a>`;
};
return marked(content, { renderer });
}
}
};
@@ -582,4 +707,171 @@ export default {
.chat-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Markdown样式 */
.ai-text h1, .ai-text h2, .ai-text h3, .ai-text h4, .ai-text h5, .ai-text h6 {
margin: 16px 0 8px 0;
font-weight: 600;
line-height: 1.4;
}
.ai-text h1 {
font-size: 1.5em;
color: #303133;
border-bottom: 2px solid #e4e7ed;
padding-bottom: 8px;
}
.ai-text h2 {
font-size: 1.3em;
color: #409eff;
}
.ai-text h3 {
font-size: 1.2em;
color: #606266;
}
.ai-text h4, .ai-text h5, .ai-text h6 {
font-size: 1.1em;
color: #909399;
}
.ai-text p {
margin: 8px 0;
line-height: 1.6;
}
.ai-text code {
background: #f5f5f5;
border: 1px solid #e4e7ed;
border-radius: 3px;
padding: 2px 6px;
font-family: 'Courier New', Consolas, monospace;
font-size: 0.9em;
color: #e74c3c;
}
.ai-text pre {
background: #2d3748;
color: #e2e8f0;
border-radius: 6px;
padding: 16px;
margin: 12px 0;
overflow-x: auto;
font-family: 'Courier New', Consolas, monospace;
font-size: 0.9em;
line-height: 1.4;
}
.ai-text pre code {
background: transparent;
border: none;
padding: 0;
color: inherit;
font-size: inherit;
}
.ai-text ul, .ai-text ol {
margin: 8px 0;
padding-left: 24px;
}
.ai-text li {
margin: 4px 0;
line-height: 1.6;
}
.ai-text ul li {
list-style-type: disc;
}
.ai-text ol li {
list-style-type: decimal;
}
.ai-text blockquote {
border-left: 4px solid #409eff;
background: #f8f9fa;
margin: 12px 0;
padding: 12px 16px;
color: #606266;
font-style: italic;
}
.ai-text table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
}
.ai-text th, .ai-text td {
border: 1px solid #e4e7ed;
padding: 8px 12px;
text-align: left;
}
.ai-text th {
background: #f5f7fa;
font-weight: 600;
}
.ai-text hr {
border: none;
border-top: 2px solid #e4e7ed;
margin: 16px 0;
}
.ai-text strong {
font-weight: 600;
color: #303133;
}
.ai-text em {
font-style: italic;
color: #606266;
}
/* 加载更多按钮样式 */
.load-more-container {
text-align: center;
padding: 20px;
border-top: 1px solid #e4e7ed;
margin-top: 16px;
}
.load-more-btn {
background: linear-gradient(135deg, #409eff 0%, #1890ff 100%);
border: none;
border-radius: 6px;
padding: 8px 24px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
transition: all 0.3s ease;
}
.load-more-btn:hover {
background: linear-gradient(135deg, #66b1ff 0%, #40a9ff 100%);
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.4);
transform: translateY(-1px);
}
.no-more-messages {
margin: 16px 0;
}
.no-more-text {
color: #909399;
font-size: 14px;
font-style: italic;
padding: 0 16px;
background: #f8f9fa;
}
.el-divider--horizontal {
margin: 16px 0;
}
</style>