feat(aitutor): 新增聊天历史分页加载和Markdown渲染功能
为聊天历史页面添加分页加载功能,支持加载更多历史消息 使用marked库实现AI回答的Markdown格式渲染 优化聊天历史对话框的样式和交互体验
This commit is contained in:
@@ -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",
|
||||
|
@@ -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;
|
||||
|
||||
// 获取当前消息列表中最早的消息ID(created_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>
|
Reference in New Issue
Block a user