lzh1232 8 months ago
commit d25c6e6a58
  1. 2
      ALOps_sys_fe/alops-ui/src/router/index.js
  2. 260
      ALOps_sys_fe/alops-ui/src/views/QandA/Aiassistant/index.vue

@ -96,7 +96,7 @@ export const dynamicRoutes = [
path: '/inMonitoring/devicesState-detail',
component: Layout,
hidden: true,
permissions: ['inMonitoring:devicesState:edit'],
permissions: ['inMonitoring:devicesState:detail'],
children: [
{
path: 'detail/:id',

@ -18,8 +18,8 @@
:class="['message', message.sender]"
>
<div class="message-content">
<div>{{ message.text }}</div>
<div class="timestamp">{{ message.time }}</div>
<div v-html="message.text"></div>
<!-- <div class="timestamp">{{ message.time }}</div> -->
</div>
</div>
</div>
@ -73,7 +73,7 @@ export default {
]
};
},mounted() {
//
//
this.scrollToBottom();
},
methods: {
@ -86,33 +86,109 @@ export default {
sender: 'customer',
time: this.formatTime(new Date())
});
//
this.scrollToBottom();
let newMessageContent = this.newMessage;
//
setTimeout(() => {
// autoReply
this.autoReply(newMessageContent);
}, 1000);
this.newMessage = '';
},
async autoReply(userMessage) {
console.log("%c 🇱🇺: autoReply -> userMessage ", "font-size:16px;background-color:#c70b31;color:white;", userMessage)
let replyText = '';
// ""
if (userMessage.includes('测试数据')) {
//
setTimeout(() => {
replyText = this.getTestData();
//
const messageIndex = this.messages.length;
this.messages.push({
text: '', //
sender: 'service',
time: this.formatTime(new Date())
});
//
this.typeWriter(messageIndex, replyText);
}, 500);
} else {
// loading
this.isLoading = true;
try {
let res = await knowledgeAsk({question: userMessage });
if (res.code == 200) {
replyText = res.data.answer;
replyText = this.formatResponse(res.data.answer);
} else {
replyText = '很抱歉,暂时无法回答您的问题。您可以尝试其他常见问题或联系客服人员。';
}
//
//
const messageIndex = this.messages.length;
this.messages.push({
text: replyText,
text: '', //
sender: 'service',
time: this.formatTime(new Date())
});
//
//
this.typeWriter(messageIndex, replyText);
} finally {
// loading
setTimeout(() => {
this.isLoading = false;
}, 300); //
}
}
},
//
typeWriter(messageIndex, fullText) {
let currentIndex = 0;
//
this.$nextTick(() => {
this.scrollToBottom();
});
const typeInterval = setInterval(() => {
if (currentIndex < fullText.length) {
//
let typeSpeed = 15; // 15/
// HTML
if (fullText[currentIndex] === '<' || (currentIndex > 0 && fullText[currentIndex-1] === '<')) {
// HTML
const tagEndIndex = fullText.indexOf('>', currentIndex);
if (tagEndIndex !== -1) {
//
this.messages[messageIndex].text = fullText.substring(0, tagEndIndex + 1);
currentIndex = tagEndIndex + 1;
} else {
//
this.messages[messageIndex].text = fullText.substring(0, currentIndex + 1);
currentIndex++;
}
} else {
//
this.messages[messageIndex].text = fullText.substring(0, currentIndex + 1);
currentIndex++;
}
//
if (currentIndex % 10 === 0 || currentIndex === fullText.length) {
this.scrollToBottom();
}
} else {
//
clearInterval(typeInterval);
//
this.scrollToBottom();
}
}, 15); // 15/
},
selectFaq(faq) {
//
@ -122,14 +198,16 @@ export default {
time: this.formatTime(new Date())
});
//
// 使
setTimeout(() => {
const messageIndex = this.messages.length;
this.messages.push({
text: faq.answer,
text: '', //
sender: 'service',
time: this.formatTime(new Date())
});
this.scrollToBottom();
// 使
this.typeWriter(messageIndex, faq.answer);
}, 1000);
},
scrollToBottom() {
@ -138,6 +216,101 @@ export default {
container.scrollTop = container.scrollHeight;
});
},
// -
scrollToTop() {
this.$nextTick(() => {
const container = this.$refs.messagesContainer;
container.scrollTop = 0;
});
},
// - MarkdownHTML
formatResponse(answer) {
if (!answer) return '';
// Markdown
return this.markdownToHtml(answer);
},
// MarkdownHTML
markdownToHtml(text) {
if (!text) return '';
//
text = text.replace(/^### (.*$)/gm, '<h3>$1</h3>');
text = text.replace(/^## (.*$)/gm, '<h2>$1</h2>');
text = text.replace(/^# (.*$)/gm, '<h1>$1</h1>');
// 线
text = text.replace(/^---$/gm, '<hr>');
//
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
//
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
//
text = text.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>');
//
text = text.replace(/^- (.*$)/gm, '<ul><li>$1</li></ul>');
//
text = text.replace(/<\/li><\/ul>\s*<ul><li>/g, '</li><li>');
//
text = text.replace(/^(\d+)\. (.*$)/gm, '<ol><li>$2</li></ol>');
//
text = text.replace(/<\/li><\/ol>\s*<ol><li>/g, '</li><li>');
//
text = text.replace(/^(?!<h|<ul|<ol|<blockquote|<hr)(.*$)/gm, '<p>$1</p>');
return text;
},
//
getTestData() {
// HTML
return `
<h3>测试数据 - 课程安排信息</h3>
<hr>
<h3>1. 选课时间安排</h3>
<ul>
<li><strong>预选时间</strong>2025年8月26日9:00 8月27日16:00</li>
<li><strong>正选时间</strong>2025年8月29日9:00 8月30日16:00</li>
</ul>
<blockquote><em>说明此为2025-2026学年秋季学期的固定选课窗口期</em></blockquote>
<h3>2. 选课方式与系统登录步骤</h3>
<ul>
<li><strong>选课方式</strong>学生必须通过<strong>电脑端</strong>登录"一网通办"平台</li>
<li>
<strong>系统登录三步法</strong>
<ol>
<li><strong>激活账号</strong>完成账号激活</li>
<li><strong>关注公众号</strong>扫码关注</li>
<li><strong>登录系统</strong>进入平台进行选课</li>
</ol>
</li>
</ul>
<h3>3. 注意事项</h3>
<ul>
<li>
<strong>体育课规则</strong>
<ul>
<li>公选课的体育课<strong>不计入</strong>体育必修课学分</li>
<li>学生毕业前须修<strong>4次体育课</strong></li>
</ul>
</li>
<li>
<strong>课程号前缀要求</strong>
<ul>
<li>校本部学生选课时<strong>课程号必须以"T"开头</strong></li>
</ul>
</li>
</ul>
`;
},
formatTime(date) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
@ -147,6 +320,7 @@ export default {
watch: {
messages: {
handler() {
//
this.scrollToBottom();
},
deep: true
@ -202,17 +376,28 @@ export default {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 10px 15px;
border-radius: 4px;
/* 固定最小高度,避免内容变化时高度抖动 */
min-height: 20px;
}
.message.customer .message-content {
background-color: #ecf5ff;
border: 1px solid #d9ecff;
background:#f5f5f5;
border-radius: 26px;
font-family: PingFang SC;
font-size: 16px;
font-weight: 400;
justify-self: end;
line-height: 28px;
max-width: 100%;
padding: 10px 20px;
white-space: pre-wrap;
word-break: break-all;
}
.message.service .message-content {
background-color: #f4f4f5;
border: 1px solid #e9e9eb;
background-color: transparent; /* 移除背景色 */
border: none; /* 移除边框 */
width: 100%; /* 设置宽度为100% */
}
.chat-input {
padding: 15px;
@ -238,4 +423,43 @@ export default {
margin-top: 5px;
text-align: right;
}
/* Loading样式 */
.loading-indicator {
display: flex;
justify-content: flex-start;
padding: 10px 0;
}
.loading-dots {
display: flex;
align-items: center;
gap: 4px;
background-color: #f4f4f5;
padding: 8px 16px;
border-radius: 4px;
border: 1px solid #e9e9eb;
}
.loading-dots .dot {
width: 8px;
height: 8px;
background-color: #409EFF;
border-radius: 50%;
animation: loading 1.4s infinite ease-in-out both;
}
.loading-dots .dot:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dots .dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loading {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
</style>
Loading…
Cancel
Save