mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-09 02:46:44 +08:00
feat(portal/case): 增强AI对话窗口交互功能
-为sendMessage组件添加textarea输入框,支持多行输入 - 为AI对话窗口添加拖拽和调整大小功能 - 在最小化窗口中添加关闭按钮- 优化窗口样式和布局,提升用户体验 - 添加拖拽手柄和窗口控制按钮 - 实现窗口位置和大小的动态调整 - 引入open.png图标用于最小化窗口操作
This commit is contained in:
@@ -14,11 +14,14 @@
|
|||||||
:fullscreen="false"
|
:fullscreen="false"
|
||||||
top="10vh"
|
top="10vh"
|
||||||
v-show="windowState === 'maximized'"
|
v-show="windowState === 'maximized'"
|
||||||
|
v-resizeable
|
||||||
|
v-draggable
|
||||||
>
|
>
|
||||||
<!-- 标题 -->
|
<!-- 标题 -->
|
||||||
<div slot="title" class="dialog-title">
|
<div slot="title" class="dialog-title">
|
||||||
<span>案例专家</span>
|
<span>案例专家</span>
|
||||||
<el-button
|
<el-button
|
||||||
|
style="color:#96999f"
|
||||||
type="text"
|
type="text"
|
||||||
class="window-control-btn"
|
class="window-control-btn"
|
||||||
@click="minimizeWindow"
|
@click="minimizeWindow"
|
||||||
@@ -75,13 +78,24 @@
|
|||||||
>
|
>
|
||||||
<div class="minimized-content">
|
<div class="minimized-content">
|
||||||
<span class="window-title">案例专家</span>
|
<span class="window-title">案例专家</span>
|
||||||
<el-button
|
<div style="display: flex;align-items: center">
|
||||||
type="text"
|
<el-button
|
||||||
class="window-control-btn"
|
type="text"
|
||||||
@click.stop="onMinimizedWindowClick"
|
class="window-control-btn"
|
||||||
>
|
@click.stop="onMinimizedWindowClick"
|
||||||
<i class="el-icon-full-screen"></i>
|
>
|
||||||
</el-button>
|
<img :src="openImg" alt="" style="width: 17px">
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
style="margin-left: 1px;color:#96999f"
|
||||||
|
type="text"
|
||||||
|
class="window-control-btn"
|
||||||
|
@click.stop="closeMinimizedWindow"
|
||||||
|
>
|
||||||
|
<i class="el-icon-close"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="minimized-message">
|
<div class="minimized-message">
|
||||||
<div v-if="messageList.length <= 1 && messageList[0].isBot">
|
<div v-if="messageList.length <= 1 && messageList[0].isBot">
|
||||||
@@ -99,7 +113,7 @@
|
|||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import messages from './components/messages.vue'
|
import messages from './components/messages.vue'
|
||||||
import sendMessage from './components/sendMessage.vue'
|
import sendMessage from './components/sendMessage.vue'
|
||||||
|
import openImg from './components/open.png'
|
||||||
export default {
|
export default {
|
||||||
name: 'CaseExpertDialog',
|
name: 'CaseExpertDialog',
|
||||||
props: {
|
props: {
|
||||||
@@ -112,6 +126,281 @@ export default {
|
|||||||
messages,
|
messages,
|
||||||
sendMessage
|
sendMessage
|
||||||
},
|
},
|
||||||
|
directives: {
|
||||||
|
draggable: {
|
||||||
|
bind(el, binding, vnode) {
|
||||||
|
vnode.context.$nextTick(() => {
|
||||||
|
const dialogEl = el.querySelector('.el-dialog');
|
||||||
|
if (!dialogEl) return;
|
||||||
|
|
||||||
|
const headerEl = dialogEl.querySelector('.dialog-title');
|
||||||
|
if (!headerEl) return;
|
||||||
|
|
||||||
|
// 设置初始样式
|
||||||
|
dialogEl.style.position = 'fixed';
|
||||||
|
dialogEl.style.top = '100px';
|
||||||
|
dialogEl.style.left = (window.innerWidth - dialogEl.offsetWidth) / 2 + 'px';
|
||||||
|
dialogEl.style.margin = '0';
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let startLeft = 0;
|
||||||
|
let startTop = 0;
|
||||||
|
|
||||||
|
const startDrag = (event) => {
|
||||||
|
// 只有在标题栏上按下鼠标才开始拖动
|
||||||
|
if (event.target.closest('.resize-handle')) {
|
||||||
|
return; // 如果点击的是resize-handle,则不触发拖动
|
||||||
|
}
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
startX = event.clientX;
|
||||||
|
startY = event.clientY;
|
||||||
|
startLeft = parseInt(dialogEl.style.left) || dialogEl.offsetLeft;
|
||||||
|
startTop = parseInt(dialogEl.style.top) || dialogEl.offsetTop;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// 添加全局事件监听
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const deltaX = event.clientX - startX;
|
||||||
|
const deltaY = event.clientY - startY;
|
||||||
|
|
||||||
|
dialogEl.style.left = (startLeft + deltaX) + 'px';
|
||||||
|
dialogEl.style.top = (startTop + deltaY) + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
// 移除全局事件监听
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 为标题栏绑定拖动事件
|
||||||
|
headerEl.addEventListener('mousedown', startDrag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resizeable: {
|
||||||
|
bind(el, binding, vnode) {
|
||||||
|
// 确保元素已插入DOM
|
||||||
|
vnode.context.$nextTick(() => {
|
||||||
|
const dialogEl = el.querySelector('.el-dialog');
|
||||||
|
if (!dialogEl) return;
|
||||||
|
|
||||||
|
// 设置初始样式
|
||||||
|
dialogEl.style.position = 'fixed';
|
||||||
|
dialogEl.style.top = '100px';
|
||||||
|
dialogEl.style.left = (window.innerWidth - dialogEl.offsetWidth) / 2 + 'px';
|
||||||
|
|
||||||
|
// 创建拖拽手柄
|
||||||
|
const createHandle = (direction) => {
|
||||||
|
const handle = document.createElement('div');
|
||||||
|
handle.className = `resize-handle ${direction}`;
|
||||||
|
handle.style.position = 'absolute';
|
||||||
|
handle.style.zIndex = '10';
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'left':
|
||||||
|
case 'right':
|
||||||
|
handle.style.width = '6px';
|
||||||
|
handle.style.height = '100%';
|
||||||
|
handle.style.top = '0';
|
||||||
|
handle.style.cursor = 'ew-resize';
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
case 'bottom':
|
||||||
|
handle.style.width = '100%';
|
||||||
|
handle.style.height = '6px';
|
||||||
|
handle.style.left = '0';
|
||||||
|
handle.style.cursor = 'ns-resize';
|
||||||
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
case 'top-right':
|
||||||
|
case 'bottom-left':
|
||||||
|
case 'bottom-right':
|
||||||
|
handle.style.width = '10px';
|
||||||
|
handle.style.height = '10px';
|
||||||
|
handle.style.zIndex = '20';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'left':
|
||||||
|
handle.style.left = '0';
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
handle.style.right = '0';
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
handle.style.top = '0';
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
handle.style.bottom = '0';
|
||||||
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
handle.style.top = '0';
|
||||||
|
handle.style.left = '0';
|
||||||
|
handle.style.cursor = 'nw-resize';
|
||||||
|
break;
|
||||||
|
case 'top-right':
|
||||||
|
handle.style.top = '0';
|
||||||
|
handle.style.right = '0';
|
||||||
|
handle.style.cursor = 'ne-resize';
|
||||||
|
break;
|
||||||
|
case 'bottom-left':
|
||||||
|
handle.style.bottom = '0';
|
||||||
|
handle.style.left = '0';
|
||||||
|
handle.style.cursor = 'sw-resize';
|
||||||
|
break;
|
||||||
|
case 'bottom-right':
|
||||||
|
handle.style.bottom = '0';
|
||||||
|
handle.style.right = '0';
|
||||||
|
handle.style.cursor = 'se-resize';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止拖拽手柄的事件冒泡到标题栏
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogEl.appendChild(handle);
|
||||||
|
return handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建8个拖拽手柄
|
||||||
|
const handles = {
|
||||||
|
left: createHandle('left'),
|
||||||
|
right: createHandle('right'),
|
||||||
|
top: createHandle('top'),
|
||||||
|
bottom: createHandle('bottom'),
|
||||||
|
topLeft: createHandle('top-left'),
|
||||||
|
topRight: createHandle('top-right'),
|
||||||
|
bottomLeft: createHandle('bottom-left'),
|
||||||
|
bottomRight: createHandle('bottom-right')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加拖拽事件处理
|
||||||
|
let isResizing = false;
|
||||||
|
let resizeDirection = '';
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let startWidth = 0;
|
||||||
|
let startHeight = 0;
|
||||||
|
let startLeft = 0;
|
||||||
|
let startTop = 0;
|
||||||
|
|
||||||
|
const startResize = (direction, event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
isResizing = true;
|
||||||
|
resizeDirection = direction;
|
||||||
|
|
||||||
|
startX = event.clientX;
|
||||||
|
startY = event.clientY;
|
||||||
|
startWidth = dialogEl.offsetWidth;
|
||||||
|
startHeight = dialogEl.offsetHeight;
|
||||||
|
|
||||||
|
// 统一使用计算后的样式值
|
||||||
|
startLeft = parseInt(dialogEl.style.left) || 0;
|
||||||
|
startTop = parseInt(dialogEl.style.top) || 0;
|
||||||
|
|
||||||
|
// 添加全局事件监听
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', stopResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const deltaX = event.clientX - startX;
|
||||||
|
const deltaY = event.clientY - startY;
|
||||||
|
|
||||||
|
switch (resizeDirection) {
|
||||||
|
case 'right':
|
||||||
|
dialogEl.style.width = Math.max(400, startWidth + deltaX) + 'px';
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
const newWidth = Math.max(400, startWidth - deltaX);
|
||||||
|
dialogEl.style.width = newWidth + 'px';
|
||||||
|
dialogEl.style.left = (startLeft + startWidth - newWidth) + 'px';
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
dialogEl.style.height = Math.max(600, startHeight + deltaY) + 'px';
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
// 当窗口高度达到最小值时,不再调整高度和位置
|
||||||
|
if (startHeight - deltaY >= 600) {
|
||||||
|
dialogEl.style.height = (startHeight - deltaY) + 'px';
|
||||||
|
dialogEl.style.top = (startTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'bottom-right':
|
||||||
|
dialogEl.style.width = Math.max(400, startWidth + deltaX) + 'px';
|
||||||
|
dialogEl.style.height = Math.max(600, startHeight + deltaY) + 'px';
|
||||||
|
break;
|
||||||
|
case 'bottom-left':
|
||||||
|
const newWidthBL = Math.max(400, startWidth - deltaX);
|
||||||
|
dialogEl.style.width = newWidthBL + 'px';
|
||||||
|
dialogEl.style.left = (startLeft + startWidth - newWidthBL) + 'px';
|
||||||
|
dialogEl.style.height = Math.max(600, startHeight + deltaY) + 'px';
|
||||||
|
break;
|
||||||
|
case 'top-right':
|
||||||
|
// 当窗口高度达到最小值时,不再调整高度和位置
|
||||||
|
if (startHeight - deltaY >= 600) {
|
||||||
|
dialogEl.style.height = (startHeight - deltaY) + 'px';
|
||||||
|
dialogEl.style.top = (startTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
dialogEl.style.width = Math.max(400, startWidth + deltaX) + 'px';
|
||||||
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
// 当窗口高度达到最小值时,不再调整高度和位置
|
||||||
|
if (startHeight - deltaY >= 600) {
|
||||||
|
dialogEl.style.height = (startHeight - deltaY) + 'px';
|
||||||
|
dialogEl.style.top = (startTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
const newWidthTL = Math.max(400, startWidth - deltaX);
|
||||||
|
dialogEl.style.width = newWidthTL + 'px';
|
||||||
|
dialogEl.style.left = (startLeft + startWidth - newWidthTL) + 'px';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
isResizing = false;
|
||||||
|
resizeDirection = '';
|
||||||
|
|
||||||
|
// 移除全局事件监听
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', stopResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 为每个手柄绑定事件
|
||||||
|
handles.left.addEventListener('mousedown', (e) => startResize('left', e));
|
||||||
|
handles.right.addEventListener('mousedown', (e) => startResize('right', e));
|
||||||
|
handles.top.addEventListener('mousedown', (e) => startResize('top', e));
|
||||||
|
handles.bottom.addEventListener('mousedown', (e) => startResize('bottom', e));
|
||||||
|
handles.topLeft.addEventListener('mousedown', (e) => startResize('top-left', e));
|
||||||
|
handles.topRight.addEventListener('mousedown', (e) => startResize('top-right', e));
|
||||||
|
handles.bottomLeft.addEventListener('mousedown', (e) => startResize('bottom-left', e));
|
||||||
|
handles.bottomRight.addEventListener('mousedown', (e) => startResize('bottom-right', e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('app', ['showAICallMinimized']),
|
...mapState('app', ['showAICallMinimized']),
|
||||||
showMinimizedWindow() {
|
showMinimizedWindow() {
|
||||||
@@ -121,6 +410,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
openImg,
|
||||||
AIContent: '',
|
AIContent: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
windowState: 'maximized', // 'maximized' 或 'minimized'
|
windowState: 'maximized', // 'maximized' 或 'minimized'
|
||||||
@@ -153,6 +443,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// / 关闭最小化窗口
|
||||||
|
closeMinimizedWindow() {
|
||||||
|
this.$store.commit('app/SET_SHOW_AI_CALL_MINIMIZED', false);
|
||||||
|
this.$store.commit('app/SET_SHOW_AI_CALL', false);
|
||||||
|
this.windowState = 'maximized';
|
||||||
|
},
|
||||||
getMinWidow(vis){
|
getMinWidow(vis){
|
||||||
// this.showAICallMinimized = vis
|
// this.showAICallMinimized = vis
|
||||||
this.windowState = 'minimized';
|
this.windowState = 'minimized';
|
||||||
@@ -165,6 +461,7 @@ export default {
|
|||||||
|
|
||||||
minimizeWindow() {
|
minimizeWindow() {
|
||||||
this.windowState = 'minimized';
|
this.windowState = 'minimized';
|
||||||
|
this.$store.commit('app/SET_SHOW_AI_CALL_MINIMIZED', true);
|
||||||
},
|
},
|
||||||
|
|
||||||
maximizeWindow() {
|
maximizeWindow() {
|
||||||
@@ -258,11 +555,14 @@ export default {
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
//background-color: rgba(255, 255, 255, 0.8);
|
//background-color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
::v-deep .el-dialog__body{
|
::v-deep .el-dialog__body{
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
flex:1;
|
||||||
//font-size: 12px;
|
//font-size: 12px;
|
||||||
*{
|
*{
|
||||||
font-size:unset ;
|
font-size:unset ;
|
||||||
@@ -279,6 +579,7 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
|
cursor: move; /* 添加拖动样式 */
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -310,7 +611,8 @@ export default {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 550px;
|
min-height: 500px;
|
||||||
|
height:100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
//margin-bottom: 20px;
|
//margin-bottom: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="case-list-content">
|
<div id="case-list-content">
|
||||||
<div style="margin-bottom:30px" class="case-banner">
|
<div style="margin-bottom:30px" class="case-banner">
|
||||||
<portal-header current="case" textColor="#fff" :goSearch="2"></portal-header>
|
<portal-header current="case" textColor="#fff" :goSearch="2">
|
||||||
|
|
||||||
|
</portal-header>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="xcontent2">
|
<div class="xcontent2">
|
||||||
|
|||||||
BIN
src/views/portal/case/components/open.png
Normal file
BIN
src/views/portal/case/components/open.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 B |
@@ -2,10 +2,13 @@
|
|||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="inputContent"
|
v-model="inputContent"
|
||||||
|
type="textarea"
|
||||||
class="input-placeholder"
|
class="input-placeholder"
|
||||||
placeholder="有问题,尽管问"
|
placeholder="有问题,尽管问"
|
||||||
@keyup.enter.native="handleSend"
|
@keyup.enter.native="handleSend"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4}"
|
||||||
|
resize="none"
|
||||||
></el-input>
|
></el-input>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<el-button type="primary" size="small" class="start-btn" @click="handleNewConversation">
|
<el-button type="primary" size="small" class="start-btn" @click="handleNewConversation">
|
||||||
|
|||||||
Reference in New Issue
Block a user