mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-11 11:56:44 +08:00
feat(portal-case): 新增AI智能问答对话功能新增消息展示组件和发送消息组件,实现机器人与用户的消息交互。
支持打字机效果展示AI回复内容,并可显示思考过程与相关案例推荐。 添加对话框背景样式及自动滚动功能,优化用户体验。 提供开启新对话和推荐问题功能,增强交互性。
This commit is contained in:
@@ -15,30 +15,41 @@
|
|||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<div class="welcome-message">
|
<div
|
||||||
<div class="avatar">
|
class="welcome-message"
|
||||||
<!-- <img src="@/assets/images/bot-avatar.png" alt="AI助手" /> -->
|
ref="messageContainer"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<div class="message-text" v-for="(item, index) in messageList" :key="index">
|
||||||
|
<messages :messageData="item" :suggestions="suggestions"></messages>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="message-suggestions" v-if="messageList[messageList.length-1].textCompleted">
|
||||||
|
<div class="suggestion-item" v-for="(item, index) in suggestions" :key="index">
|
||||||
|
<a @click="sendSuggestions"> {{ item }} →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="loading-message">
|
||||||
|
<div class="loading-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-text">
|
|
||||||
<p>您好!我是京东方案侧智能问答助手,随时为您服务。</p>
|
|
||||||
<p>我可以帮您快速查找和解读平台内的各类案例内容。只需输入您想了解的问题或关键词,我会从案例库中精准匹配相关信息,并提供清晰的解答。每条回答都会附上来源链接,方便您随时查阅原始案例全文。</p>
|
|
||||||
<p>我还会根据您的提问,智能推荐相关延伸问题,助您更高效地探索知识、解决问题。</p>
|
|
||||||
<p>现在,欢迎随时向我提问,开启高效的知识查询体验吧!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入框区域 -->
|
<!-- 输入框区域 -->
|
||||||
<div class="input-area">
|
<send-message
|
||||||
<el-input v-model="AIContent" class="input-placeholder" placeholder="有问题,尽管问"></el-input>
|
v-model="AIContent"
|
||||||
<div class="action-buttons">
|
:message-list="messageList"
|
||||||
<el-button type="primary" size="small" class="start-btn">
|
:suggestions="suggestions"
|
||||||
+ 开启新对话
|
@loading="handleLoading"
|
||||||
</el-button>
|
@update-message="updateMessage"
|
||||||
<el-button type="text" class="send-btn">
|
@update-suggestions="updateSuggestions"
|
||||||
<i class="el-icon-s-promotion"></i>
|
@new-conversation="startNewConversation"
|
||||||
</el-button>
|
:disabled="isLoading"
|
||||||
</div>
|
class="input-area-wrapper"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 关闭按钮在右上角,由 el-dialog 自动处理 -->
|
<!-- 关闭按钮在右上角,由 el-dialog 自动处理 -->
|
||||||
@@ -46,6 +57,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import messages from './components/messages.vue'
|
||||||
|
import sendMessage from './components/sendMessage.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CaseExpertDialog',
|
name: 'CaseExpertDialog',
|
||||||
props: {
|
props: {
|
||||||
@@ -54,10 +68,36 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
messages,
|
||||||
|
sendMessage
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
AIContent: '',
|
AIContent: '',
|
||||||
// dialogVisible: true // 控制弹窗显示,实际项目中可通过父组件控制
|
isLoading: false,
|
||||||
|
messageList: [
|
||||||
|
{
|
||||||
|
typing:true,
|
||||||
|
isBot: true, // 是否为机器人
|
||||||
|
text: `<p><b>您好!我是京东方案侧智能问答助手,随时为您服务。</b></p>
|
||||||
|
<p>我可以帮您快速查找和解读平台内的各类案例内容。只需输入您想了解的问题或关键词,我会从案例库中精准匹配相关信息,并提供清晰的解答。每条回答都会附上来源链接,方便您随时查阅原始案例全文。</p>
|
||||||
|
<p>我还会根据您的提问,智能推荐相关延伸问题,助您更高效地探索知识、解决问题。</p>
|
||||||
|
<p>现在,欢迎随时向我提问,开启高效的知识查询体验吧!</p>`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
suggestions:[],
|
||||||
|
isAutoScroll: true // 是否自动滚动
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
messageList: {
|
||||||
|
handler() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -65,6 +105,55 @@ export default {
|
|||||||
console.log('关闭弹窗')
|
console.log('关闭弹窗')
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
// 可以在这里执行其他逻辑
|
// 可以在这里执行其他逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理加载状态
|
||||||
|
handleLoading(status) {
|
||||||
|
this.isLoading = status;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新消息
|
||||||
|
updateMessage(message) {
|
||||||
|
// 由于Vue的响应式系统,message对象的更改会自动更新视图
|
||||||
|
// 这里不需要额外的操作
|
||||||
|
},
|
||||||
|
updateSuggestions(arr){
|
||||||
|
this.suggestions = arr
|
||||||
|
},
|
||||||
|
// 处理建议
|
||||||
|
sendSuggestions(){
|
||||||
|
this.suggestions = []
|
||||||
|
},
|
||||||
|
startNewConversation() {
|
||||||
|
this.messageList = [
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
text: `<p><b>您好!我是京东方案侧智能问答助手,随时为您服务。</b></p>
|
||||||
|
<p>我可以帮您快速查找和解读平台内的各类案例内容。只需输入您想了解的问题或关键词,我会从案例库中精准匹配相关信息,并提供清晰的解答。每条回答都会附上来源链接,方便您随时查阅原始案例全文。</p>
|
||||||
|
<p>我还会根据您的提问,智能推荐相关延伸问题,助您更高效地探索知识、解决问题。</p>
|
||||||
|
<p>现在,欢迎随时向我提问,开启高效的知识查询体验吧!</p>`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.AIContent = '';
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理滚动事件
|
||||||
|
handleScroll(event) {
|
||||||
|
const element = event.target;
|
||||||
|
// 判断是否滚动到底部
|
||||||
|
const isAtBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 1;
|
||||||
|
|
||||||
|
// 如果滚动到底部,则开启自动滚动
|
||||||
|
// 如果离开底部,则关闭自动滚动
|
||||||
|
this.isAutoScroll = isAtBottom;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
scrollToBottom() {
|
||||||
|
if (this.isAutoScroll && this.$refs.messageContainer) {
|
||||||
|
this.$refs.messageContainer.scrollTop = this.$refs.messageContainer.scrollHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +161,22 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.case-expert-dialog {
|
.case-expert-dialog {
|
||||||
|
::v-deep .el-dialog{
|
||||||
|
background: url("./components/u762.svg") no-repeat ;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
//background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
::v-deep .el-dialog__body{
|
||||||
|
padding: 10px;
|
||||||
|
//font-size: 12px;
|
||||||
|
*{
|
||||||
|
font-size:unset ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-title {
|
.dialog-title {
|
||||||
|
background: transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -86,18 +190,37 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.message-suggestions{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.suggestion-item{
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
padding: 5px 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: rgba(228, 231, 237, 1);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f9f9f9;
|
background-color: transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
// height:400px;
|
height: 550px;
|
||||||
// position:relative;
|
position: relative;
|
||||||
//margin-bottom: 20px;
|
//margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.welcome-message {
|
.welcome-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 10px;
|
||||||
|
flex:1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
@@ -115,70 +238,70 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
|
width: 100%;
|
||||||
|
//margin-bottom: 15px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
// margin: 4px 0;
|
|
||||||
// line-height: 1.6;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 12px;
|
//font-size: 14px;
|
||||||
}
|
line-height: 1.6;
|
||||||
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-area {
|
.loading-message {
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 5px 16px 10px 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
// position:absolute;
|
|
||||||
// bottom:10px;
|
|
||||||
// width:93%;
|
|
||||||
|
|
||||||
|
|
||||||
.input-placeholder {
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
::v-deep .el-input__inner {
|
|
||||||
border: none;
|
|
||||||
padding:0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
.start-btn {
|
.avatar {
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #409eff;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-btn {
|
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #409eff;
|
background-color: #007aff;
|
||||||
color: white;
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
span {
|
||||||
background-color: #3a83ff;
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #999;
|
||||||
|
margin-right: 5px;
|
||||||
|
animation: loading 1.4s infinite ease-in-out both;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: -0.16s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-area-wrapper {
|
||||||
|
//position: absolute;
|
||||||
|
//bottom: 10px;
|
||||||
|
//width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -310,9 +310,9 @@
|
|||||||
<div class="xcontent2-minor">
|
<div class="xcontent2-minor">
|
||||||
|
|
||||||
<div id="fixd-box">
|
<div id="fixd-box">
|
||||||
<div class="AI-case">
|
<div class="AI-case" style="position: relative">
|
||||||
<img src="../../../../public/images/case-logo.png" alt="">
|
<img src="../../../../public/images/case-logo.png" alt="">
|
||||||
<span @click="getAICase"></span>
|
<span @click="getAICase" style="position: absolute; top: 65px;left: 15px;z-index: 1;width: 40%;height: 30px;"></span>
|
||||||
</div>
|
</div>
|
||||||
<router-link class="the_charts" to="/case/charts">
|
<router-link class="the_charts" to="/case/charts">
|
||||||
<div class="text">排行榜</div>
|
<div class="text">排行榜</div>
|
||||||
@@ -514,6 +514,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
showAICall:false,
|
||||||
timeoutId: null,
|
timeoutId: null,
|
||||||
isTimeData: false,
|
isTimeData: false,
|
||||||
articlePageList: [],
|
articlePageList: [],
|
||||||
|
|||||||
201
src/views/portal/case/components/messages.vue
Normal file
201
src/views/portal/case/components/messages.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<!--消息渲染-->
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "message",
|
||||||
|
props: {
|
||||||
|
messageData: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
suggestions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
displayText: '',
|
||||||
|
typingTimer: null,
|
||||||
|
typingSpeed: 30 // 打字机速度(毫秒/字符)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'messageData.text': {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal && this.messageData.isBot && !this.messageData.typing) {
|
||||||
|
// this.startTyping(newVal)
|
||||||
|
this.displayText = newVal || ''
|
||||||
|
} else {
|
||||||
|
this.displayText = newVal || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
startTyping(text) {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (this.typingTimer) {
|
||||||
|
clearInterval(this.typingTimer)
|
||||||
|
this.typingTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
// this.displayText = ''
|
||||||
|
let index = 0
|
||||||
|
|
||||||
|
// 开始打字机效果
|
||||||
|
this.typingTimer = setInterval(() => {
|
||||||
|
if (index < text.length) {
|
||||||
|
this.displayText += text.charAt(index)
|
||||||
|
index++
|
||||||
|
} else {
|
||||||
|
// 打字完成,清除定时器
|
||||||
|
clearInterval(this.typingTimer)
|
||||||
|
this.typingTimer = null
|
||||||
|
}
|
||||||
|
}, this.typingSpeed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// 组件销毁前清除定时器
|
||||||
|
if (this.typingTimer) {
|
||||||
|
clearInterval(this.typingTimer)
|
||||||
|
this.typingTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="messages">
|
||||||
|
<!-- 机器人消息-->
|
||||||
|
<div v-if="messageData.isBot" class="bot-message">
|
||||||
|
<div class="bot-think" v-if="messageData.thinkText" v-html="messageData.thinkText"></div>
|
||||||
|
<div v-html="displayText"></div>
|
||||||
|
|
||||||
|
<div v-if="messageData.caseRefers && messageData.caseRefers.length > 0 && messageData.textCompleted">
|
||||||
|
<div class="case-refers">
|
||||||
|
<div class="case-refers-title">
|
||||||
|
<span>相关案例</span>
|
||||||
|
</div>
|
||||||
|
<div class="case-refers-list">
|
||||||
|
<div class="case-refers-item" v-for="item in messageData.caseRefers">
|
||||||
|
<div class="case-refers-item-title">
|
||||||
|
<a :href="'#case-' + item.caseId">{{ item.title }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="case-refers-item-author">
|
||||||
|
<span>{{ item.authorName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="case-refers-item-keywords">
|
||||||
|
<span v-for="keyword in item.keywords" :key="keyword">{{ keyword }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 非机器人消息-->
|
||||||
|
<div v-else class="user-message">
|
||||||
|
<div class="message-text">
|
||||||
|
<div v-html="messageData.text"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 推荐问题-->
|
||||||
|
|
||||||
|
<!-- <div v-if="suggestions && suggestions.length > 0">-->
|
||||||
|
<!-- <div class="suggestions">-->
|
||||||
|
<!-- <div class="suggestions-title">-->
|
||||||
|
<!-- <span>推荐问题</span>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="suggestions-list">-->
|
||||||
|
<!-- <div class="suggestions-item" v-for="item in suggestions">-->
|
||||||
|
<!-- <div class="suggestions-item-title">-->
|
||||||
|
<!-- {{item}}-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.messages {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-refers {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.case-refers-title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-refers-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
||||||
|
.case-refers-item {
|
||||||
|
//margin-right: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border: 1px solid rgba(144, 147, 153, 0.44);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
.case-refers-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-refers-item-author,
|
||||||
|
.case-refers-item-keywords {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
float: right;
|
||||||
|
padding: 5px 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: rgba(228, 231, 237, 1);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
398
src/views/portal/case/components/sendMessage.vue
Normal file
398
src/views/portal/case/components/sendMessage.vue
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-area">
|
||||||
|
<el-input
|
||||||
|
v-model="inputContent"
|
||||||
|
class="input-placeholder"
|
||||||
|
placeholder="有问题,尽管问"
|
||||||
|
@keyup.enter.native="handleSend"
|
||||||
|
:disabled="disabled"
|
||||||
|
></el-input>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" size="small" class="start-btn" @click="handleNewConversation">
|
||||||
|
+ 开启新对话
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" class="send-btn" @click="handleSend" :disabled="disabled">
|
||||||
|
<i class="el-icon-s-promotion"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import { aiChat } from '@/api/boe/aiChat.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SendMessage',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
messageList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
suggestions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputContent: this.value,
|
||||||
|
conversationId: '' // 会话ID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.inputContent = newVal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSend() {
|
||||||
|
if (!this.inputContent.trim() || this.disabled) return
|
||||||
|
|
||||||
|
// 添加用户消息到列表
|
||||||
|
const userMessage = {
|
||||||
|
isBot: false,
|
||||||
|
text: this.inputContent
|
||||||
|
};
|
||||||
|
this.messageList.push(userMessage);
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
this.$emit('loading', true);
|
||||||
|
|
||||||
|
// 调用AI聊天接口 (暂时注释掉SSE,使用模拟数据)
|
||||||
|
this.callAIChat(this.inputContent);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
this.inputContent = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// 调用AI聊天接口
|
||||||
|
callAIChat(question) {
|
||||||
|
// 创建新的AI消息对象
|
||||||
|
const aiMessage = {
|
||||||
|
isBot: true,
|
||||||
|
text: '',
|
||||||
|
thinkText: '',
|
||||||
|
caseRefers: [], // 添加caseRefers字段
|
||||||
|
textCompleted: false // 添加文字处理完成状态,默认为false
|
||||||
|
};
|
||||||
|
this.messageList.push(aiMessage);
|
||||||
|
|
||||||
|
// 模拟SSE流式响应
|
||||||
|
this.simulateSSE(question, aiMessage);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 模拟SSE流式响应
|
||||||
|
simulateSSE(question, aiMessage) {
|
||||||
|
// 模拟AI思考过程,包含think标签内容
|
||||||
|
const fullResponse =
|
||||||
|
"<think>" +
|
||||||
|
"让我思考一下如何更好地回答用户的问题。首先需要分析问题的关键点,然后查找相关的知识库内容。\n" +
|
||||||
|
"用户询问的是关于" + question + "的问题,我需要提供准确且有用的信息。\n" +
|
||||||
|
"</think>" +
|
||||||
|
"正在分析问题:" + question + "\n" +
|
||||||
|
"检索相关案例...\n" +
|
||||||
|
"匹配最佳解决方案...\n" +
|
||||||
|
|
||||||
|
"您好,我已经收到您的问题:\"" + question + "\"。\n" +
|
||||||
|
"正在分析相关内容...\n" +
|
||||||
|
"根据我的知识库,我为您提供以下解答:\n" +
|
||||||
|
"首先,我们需要了解问题的背景和关键点。\n" +
|
||||||
|
"在这个案例中,我们可以采用以下步骤来解决:\n" +
|
||||||
|
"1. 确认问题的具体表现和影响范围\n" +
|
||||||
|
"2. 分析可能的原因和影响因素\n" +
|
||||||
|
"3. 制定针对性的解决方案\n" +
|
||||||
|
"4. 实施并验证解决方案的有效性\n" +
|
||||||
|
"如果您还有其他相关问题,欢迎继续提问!";
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let inThinkTag = false;
|
||||||
|
let thinkContent = '';
|
||||||
|
let mainContent = '';
|
||||||
|
|
||||||
|
|
||||||
|
this.suggestions = [1,2,3]
|
||||||
|
|
||||||
|
this.$emit('update-suggestions', [1,2,3]);
|
||||||
|
// 模拟打字机效果
|
||||||
|
const typeNextCharacter = () => {
|
||||||
|
if (index < fullResponse.length) {
|
||||||
|
const char = fullResponse.charAt(index);
|
||||||
|
|
||||||
|
// 处理<think>标签
|
||||||
|
if (fullResponse.substring(index, index + 7) === '<think>') {
|
||||||
|
inThinkTag = true;
|
||||||
|
index += 7; // 跳过<think>标签
|
||||||
|
setTimeout(typeNextCharacter, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理</think>标签
|
||||||
|
if (fullResponse.substring(index, index + 8) === '</think>') {
|
||||||
|
inThinkTag = false;
|
||||||
|
aiMessage.thinkText = thinkContent;
|
||||||
|
index += 8; // 跳过</think>标签
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
setTimeout(typeNextCharacter, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据当前状态添加字符
|
||||||
|
if (inThinkTag) {
|
||||||
|
thinkContent += char;
|
||||||
|
aiMessage.thinkText = thinkContent;
|
||||||
|
} else {
|
||||||
|
mainContent += char;
|
||||||
|
aiMessage.text = mainContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
|
setTimeout(typeNextCharacter, 30);
|
||||||
|
// 实时更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
} else {
|
||||||
|
// 设置文字处理完成状态为true
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
|
||||||
|
// 模拟status 0的caseRefers数据
|
||||||
|
// 在消息完成后添加引用案例
|
||||||
|
setTimeout(() => {
|
||||||
|
aiMessage.caseRefers = [
|
||||||
|
{
|
||||||
|
caseId: "case_001",
|
||||||
|
title: "案例1标题",
|
||||||
|
authorName: "张三",
|
||||||
|
keywords: ["关键词1", "关键词2"],
|
||||||
|
content: "这是案例1的内容摘要..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseId: "case_002",
|
||||||
|
title: "案例2标题",
|
||||||
|
authorName: "李四",
|
||||||
|
keywords: ["关键词3", "关键词4"],
|
||||||
|
content: "这是案例2的内容摘要..."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// 所有内容完成
|
||||||
|
this.$emit('loading', false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始打字机效果
|
||||||
|
setTimeout(typeNextCharacter, 100);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 真实的SSE实现(暂时注释掉)
|
||||||
|
callAIChat(question) {
|
||||||
|
// 创建新的AI消息对象
|
||||||
|
const aiMessage = {
|
||||||
|
isBot: true,
|
||||||
|
text: '',
|
||||||
|
thinkText: '',
|
||||||
|
caseRefers: [], // 添加caseRefers字段
|
||||||
|
textCompleted: false // 添加文字处理完成状态,默认为false
|
||||||
|
};
|
||||||
|
this.messageList.push(aiMessage);
|
||||||
|
|
||||||
|
// 构造请求参数
|
||||||
|
const requestData = {
|
||||||
|
conversationId: this.conversationId,
|
||||||
|
query: question
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用fetch API处理SSE
|
||||||
|
const url = process.env.VUE_APP_BOE_BASE_API + '/xboe/m/boe/case/ai/chat';
|
||||||
|
const token = this.$store.getters.token;
|
||||||
|
|
||||||
|
// 创建POST请求
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
[this.$ajax.tokenName]: token
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
}).then(response => {
|
||||||
|
// 处理流式响应
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
let buffer = '';
|
||||||
|
let accumulatedContent = ''; // 累积的内容用于打字机效果
|
||||||
|
let accumulatedThinkContent = ''; // 累积的思考内容
|
||||||
|
let inThinkSection = false; // 是否在思考部分
|
||||||
|
|
||||||
|
// 读取流数据
|
||||||
|
const read = () => {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
// 当流结束时,设置文字处理完成状态为true
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
this.$emit('loading', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码数据
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// 按行分割数据
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop(); // 保留不完整的行
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(line.substring(6));
|
||||||
|
|
||||||
|
// 根据status处理不同类型的数据
|
||||||
|
switch (jsonData.data.status) {
|
||||||
|
case 0: // 引用文件
|
||||||
|
// 处理引用文件信息
|
||||||
|
if (jsonData.data.fileRefer && jsonData.data.fileRefer.caseRefers) {
|
||||||
|
aiMessage.caseRefers = jsonData.data.fileRefer.caseRefers;
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // 流式对话内容
|
||||||
|
// 处理<think>标签内容
|
||||||
|
const content = jsonData.data.content;
|
||||||
|
if (content.startsWith('<think>')) {
|
||||||
|
inThinkSection = true;
|
||||||
|
accumulatedThinkContent = content.replace('<think>', '');
|
||||||
|
aiMessage.thinkText = accumulatedThinkContent;
|
||||||
|
} else if (content.startsWith('</think>')) {
|
||||||
|
inThinkSection = false;
|
||||||
|
accumulatedThinkContent += content.replace('</think>', '');
|
||||||
|
aiMessage.thinkText = accumulatedThinkContent;
|
||||||
|
} else if (inThinkSection) {
|
||||||
|
accumulatedThinkContent += content;
|
||||||
|
aiMessage.thinkText = accumulatedThinkContent;
|
||||||
|
} else {
|
||||||
|
// 累积内容并更新显示
|
||||||
|
accumulatedContent += content;
|
||||||
|
aiMessage.text = accumulatedContent;
|
||||||
|
}
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // 回答完成
|
||||||
|
// 设置文字处理完成状态为true
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: // 返回建议
|
||||||
|
// 这里可以处理建议问题
|
||||||
|
console.log('建议问题:', jsonData.data.suggestions);
|
||||||
|
this.$emit('update-suggestions', []);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4: // 交互完成
|
||||||
|
this.$emit('loading', false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析SSE数据错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续读取
|
||||||
|
read();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('SSE连接错误:', error);
|
||||||
|
// 出错时也设置文字处理完成状态
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
this.$emit('loading', false);
|
||||||
|
aiMessage.text = '抱歉,网络连接出现问题,请稍后重试。';
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始读取数据
|
||||||
|
read();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
// 出错时也设置文字处理完成状态
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
this.$emit('loading', false);
|
||||||
|
aiMessage.text = '抱歉,网络连接出现问题,请稍后重试。';
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
handleNewConversation() {
|
||||||
|
this.$emit('new-conversation')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.input-area {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 5px 16px 10px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.input-placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
::v-deep .el-input__inner {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
src/views/portal/case/components/u762.svg
Normal file
12
src/views/portal/case/components/u762.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="750px" height="850px" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<radialGradient cx="362.789473684209" cy="413.96491228069874" r="1153.015055438179" gradientTransform="matrix(0.023310357358899587 0.999728276703125 -0.9997282767031253 0.02331035735889959 768.1851497765263 41.62434690904212 )" gradientUnits="userSpaceOnUse" id="RadialGradient4">
|
||||||
|
<stop id="Stop5" stop-color="#ffffff" offset="0" />
|
||||||
|
<stop id="Stop6" stop-color="#d4def7" offset="1" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<path d="M 0 5.000000000000001 A 5 5 0 0 1 4.999999999999999 0 L 745 0 A 5 5 0 0 1 750 5 L 750 845 A 5 5 0 0 1 745 850 L 5 850 A 5 5 0 0 1 0 845 L 0 5 Z " fill-rule="nonzero" fill="url(#RadialGradient4)" stroke="none" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 858 B |
Reference in New Issue
Block a user