mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-09 02:46:44 +08:00
- 将AI对话框宽度从65%调整为800px- 修改初始欢迎消息格式,使用HTML标签替换Markdown语法-修复机器人消息显示逻辑,确保displayText正确赋值- 优化消息渲染流程,提升用户体验
421 lines
10 KiB
Vue
421 lines
10 KiB
Vue
<template>
|
||
<div class="messages">
|
||
<!-- 机器人消息 -->
|
||
<div v-if="messageData.isBot" class="bot-message">
|
||
<!-- 思考中提示 -->
|
||
<div v-if="messageData.thinkText" class="bot-think" v-katex:auto v-html="md.render(messageData.thinkText)"></div>
|
||
|
||
<!-- 主要回复内容 -->
|
||
<div
|
||
ref="contentContainer"
|
||
class="message-content"
|
||
v-katex:auto
|
||
v-html="displayText"
|
||
></div>
|
||
|
||
<!-- 引用案例 -->
|
||
<div v-if="messageData.caseRefers && messageData.caseRefers.length > 0 && messageData.textCompleted" class="case-refers">
|
||
<div class="case-refers-title">
|
||
<span><i class="iconfont icon-think"></i> 引用案例</span>
|
||
<span v-if="shouldShowMoreButton" class="more" @click="toggleShowAllCaseRefers">
|
||
{{ showAllCaseRefers ? '收起' : '查看更多' }}
|
||
</span>
|
||
</div>
|
||
<div class="case-refers-list">
|
||
<div
|
||
v-for="item in displayedCaseRefers"
|
||
:key="item.caseId"
|
||
class="case-refers-item"
|
||
>
|
||
<div class="case-refers-item-title">
|
||
<a @click="toUrl(item)" class="title">{{ item.title }}</a>
|
||
<span class="case-refers-item-timer">{{ item.uploadTime }}</span>
|
||
</div>
|
||
<div class="case-refers-item-author">
|
||
<span class="user"></span>
|
||
<span>{{ item.authorName }}</span>
|
||
<span class="case-inter-orginInfo">{{ item.orgInfo }}</span>
|
||
</div>
|
||
<div class="case-refers-item-keywords">
|
||
<span v-for="keyword in item.keywords" :key="keyword">{{ keyword }}</span>
|
||
</div>
|
||
<div class="message-content case-content" v-html="md.render(item.content)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户消息 -->
|
||
<div v-else class="user-message">
|
||
<div class="message-text" v-html="messageData.text"></div>
|
||
</div>
|
||
|
||
<!-- 推荐问题 -->
|
||
<div v-if="suggestions && suggestions.length > 0" class="suggestions">
|
||
<div class="suggestions-title">💡 推荐问题</div>
|
||
<div class="suggestions-list">
|
||
<button
|
||
v-for="(item, index) in suggestions"
|
||
:key="index"
|
||
class="suggestions-item"
|
||
@click="$emit('suggestion-click', item)"
|
||
>
|
||
{{ item }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import MarkdownIt from 'markdown-it';
|
||
import highlight from 'markdown-it-highlightjs';
|
||
import 'highlight.js/styles/a11y-dark.css';
|
||
import markdownItMermaid from 'markdown-it-mermaid';
|
||
import mermaid from 'mermaid';
|
||
|
||
// 初始化 Mermaid
|
||
mermaid.initialize({ startOnLoad: false });
|
||
|
||
const md = new MarkdownIt({
|
||
html: true,
|
||
linkify: true,
|
||
typographer: true,
|
||
});
|
||
|
||
md.use(highlight).use(markdownItMermaid);
|
||
|
||
export default {
|
||
name: 'Message',
|
||
props: {
|
||
messageData: {
|
||
type: Object,
|
||
required: true,
|
||
default: () => ({}),
|
||
},
|
||
suggestions: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
md,
|
||
displayText: '',
|
||
typingTimer: null,
|
||
typingSpeed: 30, // 毫秒/字符
|
||
showAllCaseRefers: false,
|
||
};
|
||
},
|
||
computed: {
|
||
displayedCaseRefers() {
|
||
if (this.showAllCaseRefers || !this.messageData.caseRefers) {
|
||
return this.messageData.caseRefers || [];
|
||
}
|
||
return this.messageData.caseRefers.slice(0, 3);
|
||
},
|
||
shouldShowMoreButton() {
|
||
return this.messageData.caseRefers && this.messageData.caseRefers.length > 3;
|
||
},
|
||
},
|
||
watch: {
|
||
'messageData.text': {
|
||
handler(newVal) {
|
||
if (!newVal) {
|
||
this.displayText = '';
|
||
return;
|
||
}
|
||
|
||
if (this.messageData.isBot && !this.messageData.typing) {
|
||
this.startTyping(newVal); // 启动打字机效果
|
||
|
||
this.displayText = newVal || ''
|
||
} else {
|
||
this.displayText = this.md.render(newVal);
|
||
this.$nextTick(this.renderMermaid); // 直接渲染 Mermaid
|
||
}
|
||
},
|
||
immediate: true,
|
||
},
|
||
},
|
||
methods: {
|
||
toUrl(item) {
|
||
this.$router.push({
|
||
path: '/case/detail',
|
||
query: { id: item.caseId },
|
||
});
|
||
},
|
||
|
||
// 正确的打字机效果:先整体渲染 Markdown,再逐字显示 HTML
|
||
startTyping(fullText) {
|
||
const renderedText = this.md.render(fullText);
|
||
this.displayText = '';
|
||
let index = 0;
|
||
|
||
if (this.typingTimer) {
|
||
clearInterval(this.typingTimer);
|
||
}
|
||
|
||
this.typingTimer = setInterval(() => {
|
||
if (index < renderedText.length) {
|
||
this.displayText += renderedText[index];
|
||
index++;
|
||
} else {
|
||
clearInterval(this.typingTimer);
|
||
this.typingTimer = null;
|
||
this.$nextTick(this.renderMermaid); // 渲染 Mermaid 图表
|
||
}
|
||
}, this.typingSpeed);
|
||
},
|
||
|
||
// 触发 Mermaid 渲染
|
||
renderMermaid() {
|
||
this.$nextTick(() => {
|
||
const mermaidEls = this.$el.querySelectorAll('.mermaid');
|
||
if (mermaidEls.length > 0) {
|
||
try {
|
||
// mermaid 8.x 版本使用 init 方法而不是 run
|
||
if (typeof mermaid.init === 'function') {
|
||
mermaid.init(undefined, '.mermaid');
|
||
} else if (mermaid.default && typeof mermaid.default.init === 'function') {
|
||
mermaid.default.init(undefined, '.mermaid');
|
||
}
|
||
} catch (e) {
|
||
console.warn('Mermaid 渲染失败:', e);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 切换案例引用显示数量
|
||
toggleShowAllCaseRefers() {
|
||
this.showAllCaseRefers = !this.showAllCaseRefers;
|
||
// 切换后重新渲染 Mermaid(如果内容中有图表)
|
||
this.$nextTick(this.renderMermaid);
|
||
},
|
||
},
|
||
beforeDestroy() {
|
||
if (this.typingTimer) {
|
||
clearInterval(this.typingTimer);
|
||
this.typingTimer = null;
|
||
}
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
::v-deep .mermaid{
|
||
width: 100%;
|
||
height: auto;
|
||
}
|
||
::v-deep svg[id^="mermaid-"]{
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.messages {
|
||
width: 100%;
|
||
word-wrap: break-word;
|
||
|
||
.bot-message {
|
||
background-color: #fff;
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
margin-bottom: 10px;
|
||
|
||
.bot-think {
|
||
color: #909399;
|
||
padding-bottom: 5px;
|
||
position: relative;
|
||
padding-left: 10px;
|
||
|
||
&:before {
|
||
content: ' ';
|
||
border-left: 0.5px solid #909399;
|
||
position: absolute;
|
||
height: 100%;
|
||
left: 0;
|
||
top: -3px;
|
||
transform: scaleX(0.5);
|
||
}
|
||
}
|
||
|
||
.message-content {
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.case-content {
|
||
font-size: 12px !important;
|
||
margin-top: 8px;
|
||
padding: 6px 10px;
|
||
background-color: #f9f9f9;
|
||
border-radius: 4px;
|
||
border: 1px solid #eee;
|
||
}
|
||
}
|
||
|
||
.user-message {
|
||
float: right;
|
||
padding: 8px 15px;
|
||
max-width: 80%;
|
||
background-color: #e4e7ed;
|
||
border-radius: 18px;
|
||
margin-bottom: 10px;
|
||
font-size: 14px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
/* ========== 案例引用样式 ========== */
|
||
.case-refers {
|
||
margin-top: 12px;
|
||
|
||
.case-refers-title {
|
||
font-weight: bold;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
color: #333;
|
||
|
||
.icon-think {
|
||
background-image: url('./map.svg');
|
||
width: 15px;
|
||
height: 13px;
|
||
display: inline-block;
|
||
background-repeat: no-repeat;
|
||
background-size: 100% 100%;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.more {
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
background-color: #f4f7fd;
|
||
border-radius: 5px;
|
||
color: #577ee1;
|
||
font-weight: normal;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.case-refers-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.case-refers-item {
|
||
border: 1px solid rgba(144, 147, 153, 0.44);
|
||
padding: 8px;
|
||
border-radius: 6px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
|
||
.case-refers-item-title {
|
||
font-size: 14px;
|
||
margin-bottom: 6px;
|
||
font-weight: 600;
|
||
color: #000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.title {
|
||
max-width: 70%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: #1a73e8;
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.case-refers-item-timer {
|
||
font-size: 11px;
|
||
color: #aaa;
|
||
font-weight: normal;
|
||
}
|
||
}
|
||
|
||
.case-refers-item-author {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #555;
|
||
margin-bottom: 5px;
|
||
|
||
.user {
|
||
background-image: url('./user.svg');
|
||
width: 15px;
|
||
height: 15px;
|
||
display: inline-block;
|
||
background-repeat: no-repeat;
|
||
background-size: 100% 100%;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.case-inter-orginInfo {
|
||
font-size: 11px;
|
||
color: #999;
|
||
margin-left: 6px;
|
||
}
|
||
}
|
||
|
||
.case-refers-item-keywords {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
|
||
span {
|
||
padding: 2px 6px;
|
||
background-color: #f4f7fd;
|
||
border-radius: 5px;
|
||
font-size: 11px !important;
|
||
color: #577ee1;
|
||
}
|
||
|
||
span + span {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ========== 推荐问题 ========== */
|
||
.suggestions {
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
|
||
.suggestions-title {
|
||
font-weight: bold;
|
||
margin-bottom: 6px;
|
||
color: #333;
|
||
}
|
||
|
||
.suggestions-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
.suggestions-item {
|
||
padding: 6px 10px;
|
||
background-color: #f0f4fc;
|
||
border: none;
|
||
border-radius: 6px;
|
||
text-align: left;
|
||
color: #1a73e8;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: background-color 0.2s;
|
||
|
||
&:hover {
|
||
background-color: #e1e8f5;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|