feat(portal): 实现AI对话窗口最小化功能

- 新增窗口最小化与最大化切换功能
- 调整对话框结构支持两种显示状态
-优化消息建议显示条件判断
- 修改API地址为本地开发环境地址- 更新错误提示文案提升用户体验
- 移除旧版AI入口相关逻辑和样式
- 引入新的AICaseConsult组件替换原有实现
- 修复消息列表为空时的默认展示逻辑
- 添加getLastUserMessage方法提取纯文本内容- 优化窗口控制按钮样式和交互逻辑
This commit is contained in:
陈昱达
2025-09-28 16:39:55 +08:00
parent e3422d15ee
commit a3dab45af0
4 changed files with 181 additions and 99 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}); });