mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-11 20:06:44 +08:00
feat(portal): 实现AI对话窗口最小化功能
- 新增窗口最小化与最大化切换功能 - 调整对话框结构支持两种显示状态 -优化消息建议显示条件判断 - 修改API地址为本地开发环境地址- 更新错误提示文案提升用户体验 - 移除旧版AI入口相关逻辑和样式 - 引入新的AICaseConsult组件替换原有实现 - 修复消息列表为空时的默认展示逻辑 - 添加getLastUserMessage方法提取纯文本内容- 优化窗口控制按钮样式和交互逻辑
This commit is contained in:
@@ -1,60 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<div>
|
||||||
:visible="dialogVisible"
|
<!-- 最大化状态的弹窗 -->
|
||||||
width="600px"
|
<el-dialog
|
||||||
:close-on-click-modal="false"
|
v-if="dialogVisible"
|
||||||
:show-close="true"
|
:visible="true"
|
||||||
@close="onClose"
|
width="800px"
|
||||||
class="case-expert-dialog"
|
:close-on-click-modal="false"
|
||||||
>
|
:show-close="true"
|
||||||
<!-- 标题 -->
|
@close="onClose"
|
||||||
<div slot="title" class="dialog-title">
|
class="case-expert-dialog"
|
||||||
<!-- <img src="@/assets/images/case-expert-icon.png" alt="案例专家" class="icon" /> -->
|
:modal="false"
|
||||||
<span>案例专家</span>
|
:append-to-body="true"
|
||||||
</div>
|
:fullscreen="false"
|
||||||
|
top="10vh"
|
||||||
<!-- 内容区域 -->
|
v-show="windowState === 'maximized'"
|
||||||
<div class="content-wrapper">
|
>
|
||||||
<div
|
<!-- 标题 -->
|
||||||
class="welcome-message"
|
<div slot="title" class="dialog-title">
|
||||||
ref="messageContainer"
|
<span>案例专家</span>
|
||||||
@scroll="handleScroll"
|
<el-button
|
||||||
>
|
type="text"
|
||||||
<div class="message-text" v-for="(item, index) in messageList" :key="index">
|
class="window-control-btn"
|
||||||
<messages :messageData="item" :suggestions="suggestions"></messages>
|
@click="minimizeWindow"
|
||||||
|
>
|
||||||
</div>
|
<i class="el-icon-minus"></i>
|
||||||
<div class="message-suggestions" v-if="messageList[messageList.length-1].textCompleted">
|
</el-button>
|
||||||
<div class="suggestion-item" v-for="(item, index) in suggestions" :key="index">
|
|
||||||
<a @click="sendSuggestions(item)"> {{ item }} →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isLoading" class="loading-message">
|
|
||||||
<div class="loading-dots">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入框区域 -->
|
<!-- 内容区域 -->
|
||||||
<send-message
|
<div class="content-wrapper">
|
||||||
v-model="AIContent"
|
<div
|
||||||
:message-list="messageList"
|
class="welcome-message"
|
||||||
:suggestions="suggestions"
|
ref="messageContainer"
|
||||||
@loading="handleLoading"
|
@scroll="handleScroll"
|
||||||
@update-message="updateMessage"
|
>
|
||||||
@update-suggestions="updateSuggestions"
|
<div class="message-text" v-for="(item, index) in messageList" :key="index">
|
||||||
@new-conversation="startNewConversation"
|
<messages :messageData="item" :suggestions="suggestions"></messages>
|
||||||
:disabled="isLoading"
|
</div>
|
||||||
class="input-area-wrapper"
|
<div class="message-suggestions" v-if="messageList.length > 0 && messageList[messageList.length-1].textCompleted">
|
||||||
ref="sendMessage"
|
<div class="suggestion-item" v-for="(item, index) in suggestions" :key="index">
|
||||||
/>
|
<a @click="sendSuggestions(item)"> {{ item }} →</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="loading-message">
|
||||||
|
<div class="loading-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 关闭按钮在右上角,由 el-dialog 自动处理 -->
|
<!-- 输入框区域 -->
|
||||||
</el-dialog>
|
<send-message
|
||||||
|
v-model="AIContent"
|
||||||
|
:message-list="messageList"
|
||||||
|
:suggestions="suggestions"
|
||||||
|
@loading="handleLoading"
|
||||||
|
@update-message="updateMessage"
|
||||||
|
@update-suggestions="updateSuggestions"
|
||||||
|
@new-conversation="startNewConversation"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="input-area-wrapper"
|
||||||
|
ref="sendMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 最小化状态的弹窗 -->
|
||||||
|
<div
|
||||||
|
v-if="dialogVisible"
|
||||||
|
class="minimized-window"
|
||||||
|
v-show="windowState === 'minimized'"
|
||||||
|
@click="maximizeWindow"
|
||||||
|
>
|
||||||
|
<div class="minimized-content">
|
||||||
|
<span class="window-title">案例专家</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
class="window-control-btn"
|
||||||
|
@click.stop="maximizeWindow"
|
||||||
|
>
|
||||||
|
<i class="el-icon-full-screen"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="minimized-message">
|
||||||
|
<div v-if="messageList.length <= 1 && messageList[0].isBot">
|
||||||
|
当前暂无对话内容,去创建对话吧
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ getLastUserMessage() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -77,6 +116,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
AIContent: '',
|
AIContent: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
windowState: 'maximized', // 'maximized' 或 'minimized'
|
||||||
messageList: [
|
messageList: [
|
||||||
{
|
{
|
||||||
typing:true,
|
typing:true,
|
||||||
@@ -92,6 +132,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
dialogVisible(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
// 每次打开时默认最大化
|
||||||
|
this.windowState = 'maximized';
|
||||||
|
}
|
||||||
|
},
|
||||||
messageList: {
|
messageList: {
|
||||||
handler() {
|
handler() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -108,6 +154,27 @@ export default {
|
|||||||
// 可以在这里执行其他逻辑
|
// 可以在这里执行其他逻辑
|
||||||
},
|
},
|
||||||
|
|
||||||
|
minimizeWindow() {
|
||||||
|
this.windowState = 'minimized';
|
||||||
|
},
|
||||||
|
|
||||||
|
maximizeWindow() {
|
||||||
|
this.windowState = 'maximized';
|
||||||
|
},
|
||||||
|
|
||||||
|
getLastUserMessage() {
|
||||||
|
// 从后往前找用户消息
|
||||||
|
for (let i = this.messageList.length - 1; i >= 0; i--) {
|
||||||
|
if (!this.messageList[i].isBot) {
|
||||||
|
// 移除HTML标签只返回纯文本
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = this.messageList[i].text;
|
||||||
|
return tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
// 处理加载状态
|
// 处理加载状态
|
||||||
handleLoading(status) {
|
handleLoading(status) {
|
||||||
this.isLoading = status;
|
this.isLoading = status;
|
||||||
@@ -170,6 +237,8 @@ export default {
|
|||||||
::v-deep .el-dialog{
|
::v-deep .el-dialog{
|
||||||
background: url("./components/u762.svg") no-repeat ;
|
background: url("./components/u762.svg") no-repeat ;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
//background-color: rgba(255, 255, 255, 0.8);
|
//background-color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
@@ -185,15 +254,23 @@ export default {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
padding-right: 20px;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window-control-btn {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #000000; /* 黑色图标 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -310,4 +387,45 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.minimized-window {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 300px;
|
||||||
|
background: url("./components/u762.svg") no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 2000;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.minimized-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
.window-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-control-btn {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
color: #000000; /* 黑色图标 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimized-message {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -310,10 +310,7 @@
|
|||||||
<div class="xcontent2-minor">
|
<div class="xcontent2-minor">
|
||||||
|
|
||||||
<div id="fixd-box">
|
<div id="fixd-box">
|
||||||
<div class="AI-case" style="position: relative" v-if="showAiCase " @click="getAICase">
|
<AICaseConsult />
|
||||||
<img src="../../../../public/images/case-logo.png" alt="">
|
|
||||||
<span @click="getAICase" style="position: absolute; top: 65px;left: 15px;z-index: 1;width: 40%;height: 30px;"></span>
|
|
||||||
</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>
|
||||||
<div class="icon">></div>
|
<div class="icon">></div>
|
||||||
@@ -480,7 +477,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
<AICall :dialogVisible="showAICall" @close="onClose" />
|
<!-- <AICall :dialogVisible="showAICall" @close="onClose" />-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -501,8 +498,7 @@ import apiType from "@/api/modules/type.js";
|
|||||||
import { cutFullName } from "@/utils/tools.js";
|
import { cutFullName } from "@/utils/tools.js";
|
||||||
import apiPlace from "@/api/phase2/place.js"
|
import apiPlace from "@/api/phase2/place.js"
|
||||||
import AICall from '@/views/portal/case/AICall.vue'
|
import AICall from '@/views/portal/case/AICall.vue'
|
||||||
import { showCaseAiEntrance } from '@/api/boe/aiChat.js'
|
import AICaseConsult from '@/views/portal/case/components/AICaseConsult.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "case",
|
name: "case",
|
||||||
components: {
|
components: {
|
||||||
@@ -512,12 +508,12 @@ export default {
|
|||||||
interactBar,
|
interactBar,
|
||||||
timeShow,
|
timeShow,
|
||||||
author,
|
author,
|
||||||
AICall
|
AICall,
|
||||||
|
AICaseConsult
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showAiCase:false,
|
|
||||||
showAICall:false,
|
|
||||||
timeoutId: null,
|
timeoutId: null,
|
||||||
isTimeData: false,
|
isTimeData: false,
|
||||||
articlePageList: [],
|
articlePageList: [],
|
||||||
@@ -790,7 +786,6 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
let $this = this;
|
let $this = this;
|
||||||
this.getShowAiCase()
|
|
||||||
// if(this.speciData.length==0){
|
// if(this.speciData.length==0){
|
||||||
// this.specialized();
|
// this.specialized();
|
||||||
// }
|
// }
|
||||||
@@ -876,13 +871,6 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 是否展示入口
|
// 是否展示入口
|
||||||
getShowAiCase(){
|
|
||||||
showCaseAiEntrance().then(res=>{
|
|
||||||
this.showAiCase = res.data
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
allRequests() {
|
allRequests() {
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"scroll",
|
"scroll",
|
||||||
@@ -1904,12 +1892,7 @@ export default {
|
|||||||
this.$router.push(`/case/detail?id=${item.id}`);
|
this.$router.push(`/case/detail?id=${item.id}`);
|
||||||
},
|
},
|
||||||
// 案例立即咨询
|
// 案例立即咨询
|
||||||
getAICase() {
|
|
||||||
this.showAICall = true
|
|
||||||
},
|
|
||||||
onClose() {
|
|
||||||
this.showAICall = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -2884,22 +2867,4 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.AI-case {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
width: 160px;
|
|
||||||
height: 40px;
|
|
||||||
position: absolute;
|
|
||||||
left: 20px;
|
|
||||||
top: 105px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
{{messageData}}
|
|
||||||
<!-- 机器人消息-->
|
<!-- 机器人消息-->
|
||||||
<div v-if="messageData.isBot" class="bot-message">
|
<div v-if="messageData.isBot" class="bot-message">
|
||||||
<div class="bot-think" v-if="messageData.thinkText" v-html="messageData.thinkText"></div>
|
<div class="bot-think" v-if="messageData.thinkText" v-html="messageData.thinkText"></div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 创建POST请求
|
// 创建POST请求
|
||||||
fetch('/systemapi/xboe/m/boe/case/ai/chat',{
|
fetch('http://192.168.3.178:9091/xboe/m/boe/case/ai/chat',{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -310,7 +310,7 @@ export default {
|
|||||||
}
|
}
|
||||||
aiMessage.textCompleted = true;
|
aiMessage.textCompleted = true;
|
||||||
this.$emit('loading', false);
|
this.$emit('loading', false);
|
||||||
aiMessage.text = '抱歉,网络连接出现问题,请稍后重试。';
|
aiMessage.text = '当前无法获取回答,请稍后重试';
|
||||||
// 更新父组件的messageList
|
// 更新父组件的messageList
|
||||||
this.$emit('update-message', aiMessage);
|
this.$emit('update-message', aiMessage);
|
||||||
});
|
});
|
||||||
@@ -323,7 +323,7 @@ export default {
|
|||||||
// 出错时也设置文字处理完成状态
|
// 出错时也设置文字处理完成状态
|
||||||
aiMessage.textCompleted = true;
|
aiMessage.textCompleted = true;
|
||||||
this.$emit('loading', false);
|
this.$emit('loading', false);
|
||||||
aiMessage.text = '抱歉,网络连接出现问题,请稍后重试。';
|
aiMessage.text = '当前无法获取回答,请稍后重试';
|
||||||
// 更新父组件的messageList
|
// 更新父组件的messageList
|
||||||
this.$emit('update-message', aiMessage);
|
this.$emit('update-message', aiMessage);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user