Files
learning-system-portal/src/views/portal/case/components/messages.vue
陈昱达 86e25f69f9 feat(portal): 添加消息组件最小化窗口功能
- 在消息组件中新增 getMinWindow事件触发
- 在 AI 呼叫页面监听并处理最小化事件
- 实现窗口状态管理,支持最小化显示
-优化组件间通信逻辑,确保状态同步准确- 修复可能引起状态异常的注释代码问题- 调整样式以适配最小化窗口显示效果
2025-10-23 10:22:40 +08:00

424 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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="md.render(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 },
});
this.$emit('getMinWindow')
},
// 正确的打字机效果:先整体渲染 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: 5px;
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: #000;
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>