diff --git a/package.json b/package.json index 38422c2..b95bf71 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/views/aitutor/chathistory/index.vue b/src/views/aitutor/chathistory/index.vue index 19455ec..42c9e8c 100644 --- a/src/views/aitutor/chathistory/index.vue +++ b/src/views/aitutor/chathistory/index.vue @@ -135,10 +135,30 @@
{{ message.answer }}
+ }" v-html="renderMarkdown(message.answer)"> + + +
+ + 加载更多历史消息 + + +
+ + 已经到底了 + +
+
@@ -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 `${text}`; + }; + + 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; +} \ No newline at end of file