mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-22 01:28:12 +00:00
feat(ai-assistant): 支持拖动边缘调整聊天窗口大小
- 添加 8 个方向的调整大小控制点(四边 + 四角) - 支持从任意边缘或角落拖动调整窗口尺寸 - 尺寸自动保存到 IndexedDB,下次打开时恢复 - 窗口大小限制:最小 380×400,最大 800×900 - 视口尺寸变化时自动调整窗口大小和位置
This commit is contained in:
parent
87dd07ef23
commit
a3caf5ebdf
@ -1305,10 +1305,6 @@ export default {
|
||||
.ai-assistant-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 266px);
|
||||
@media (height <= 900px) {
|
||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 136px);
|
||||
}
|
||||
|
||||
.ai-assistant-welcome,
|
||||
.ai-assistant-output {
|
||||
@ -1575,13 +1571,12 @@ export default {
|
||||
|
||||
.ai-assistant-chat {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
width: 460px;
|
||||
height: 80vh;
|
||||
height: 600px;
|
||||
min-width: 380px;
|
||||
max-width: 600px;
|
||||
max-height: 640px;
|
||||
max-width: 800px;
|
||||
min-height: 400px;
|
||||
max-height: 900px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.12);
|
||||
border-radius: 16px;
|
||||
@ -1589,6 +1584,72 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// 调整大小控制点基础样式
|
||||
.ai-assistant-resize-handle {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 四边控制点
|
||||
.ai-assistant-resize-n {
|
||||
top: 0;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 6px;
|
||||
cursor: n-resize;
|
||||
}
|
||||
.ai-assistant-resize-s {
|
||||
bottom: 0;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 6px;
|
||||
cursor: s-resize;
|
||||
}
|
||||
.ai-assistant-resize-e {
|
||||
top: 8px;
|
||||
right: 0;
|
||||
bottom: 8px;
|
||||
width: 6px;
|
||||
cursor: e-resize;
|
||||
}
|
||||
.ai-assistant-resize-w {
|
||||
top: 8px;
|
||||
left: 0;
|
||||
bottom: 8px;
|
||||
width: 6px;
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
// 四角控制点
|
||||
.ai-assistant-resize-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: ne-resize;
|
||||
}
|
||||
.ai-assistant-resize-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: nw-resize;
|
||||
}
|
||||
.ai-assistant-resize-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.ai-assistant-resize-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.ai-assistant-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
@ -1631,14 +1692,10 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-height: 650px) {
|
||||
justify-content: normal;
|
||||
}
|
||||
|
||||
.ai-assistant-welcome-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -1669,6 +1726,7 @@ export default {
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
padding: 0 8px;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.ai-assistant-prompt-card {
|
||||
@ -1735,6 +1793,12 @@ export default {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
.ai-assistant-content {
|
||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 266px);
|
||||
@media (height <= 900px) {
|
||||
max-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 136px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.dark-mode-reverse {
|
||||
|
||||
@ -9,10 +9,19 @@
|
||||
<Icon class="ai-assistant-close" type="ios-close" @click="onClose"/>
|
||||
<div
|
||||
class="ai-assistant-drag-handle"
|
||||
@mousedown.stop.prevent="onMouseDown">
|
||||
@mousedown.stop.prevent="onDragMouseDown">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<!-- 调整大小的控制点 -->
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-n" @mousedown.stop.prevent="onResizeMouseDown($event, 'n')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-s" @mousedown.stop.prevent="onResizeMouseDown($event, 's')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-e" @mousedown.stop.prevent="onResizeMouseDown($event, 'e')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-w" @mousedown.stop.prevent="onResizeMouseDown($event, 'w')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-ne" @mousedown.stop.prevent="onResizeMouseDown($event, 'ne')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-nw" @mousedown.stop.prevent="onResizeMouseDown($event, 'nw')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-se" @mousedown.stop.prevent="onResizeMouseDown($event, 'se')"></div>
|
||||
<div class="ai-assistant-resize-handle ai-assistant-resize-sw" @mousedown.stop.prevent="onResizeMouseDown($event, 'sw')"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -64,11 +73,31 @@ export default {
|
||||
dragging: false,
|
||||
positionLoaded: false,
|
||||
cacheKey: 'aiAssistant.chatPosition',
|
||||
sizeCacheKey: 'aiAssistant.chatSize',
|
||||
// 窗口尺寸(用于计算位置)
|
||||
windowSize: {
|
||||
width: 460,
|
||||
height: 640,
|
||||
height: 600,
|
||||
},
|
||||
// 用户自定义尺寸
|
||||
customSize: {
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
// 尺寸限制
|
||||
minSize: {
|
||||
width: 380,
|
||||
height: 400,
|
||||
},
|
||||
maxSize: {
|
||||
width: 800,
|
||||
height: 900,
|
||||
},
|
||||
record: {},
|
||||
// 调整大小相关
|
||||
resizing: false,
|
||||
resizeDirection: null,
|
||||
resizeRecord: {},
|
||||
};
|
||||
},
|
||||
|
||||
@ -83,11 +112,11 @@ export default {
|
||||
},
|
||||
|
||||
clientWidth() {
|
||||
return this.windowWidth || document.documentElement.clientWidth;
|
||||
return this.windowWidth;
|
||||
},
|
||||
|
||||
clientHeight() {
|
||||
return this.windowHeight || document.documentElement.clientHeight;
|
||||
return this.windowHeight;
|
||||
},
|
||||
|
||||
// 计算实际的 left 值
|
||||
@ -112,10 +141,18 @@ export default {
|
||||
opacity: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
const style = {
|
||||
left: `${this.left}px`,
|
||||
top: `${this.top}px`,
|
||||
};
|
||||
// 应用自定义尺寸
|
||||
if (this.customSize.width) {
|
||||
style.width = `${this.customSize.width}px`;
|
||||
}
|
||||
if (this.customSize.height) {
|
||||
style.height = `${this.customSize.height}px`;
|
||||
}
|
||||
return style;
|
||||
},
|
||||
},
|
||||
|
||||
@ -127,17 +164,23 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
windowWidth() {
|
||||
this.onViewportChange();
|
||||
},
|
||||
windowHeight() {
|
||||
this.onViewportChange();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadPosition();
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.loadSizeAndPosition();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
document.removeEventListener('mousemove', this.onDragMouseMove);
|
||||
document.removeEventListener('mouseup', this.onDragMouseUp);
|
||||
document.removeEventListener('mousemove', this.onResizeMouseMove);
|
||||
document.removeEventListener('mouseup', this.onResizeMouseUp);
|
||||
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||
},
|
||||
|
||||
@ -206,40 +249,40 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 鼠标按下
|
||||
* 拖动:鼠标按下
|
||||
*/
|
||||
onMouseDown(e) {
|
||||
onDragMouseDown(e) {
|
||||
// 只响应鼠标左键
|
||||
if (e.button !== 0) return;
|
||||
|
||||
this.updateWindowSize();
|
||||
this.record = {
|
||||
time: Date.now(),
|
||||
startLeft: this.left,
|
||||
startTop: this.top,
|
||||
offsetX: e.clientX - this.left,
|
||||
offsetY: e.clientY - this.top,
|
||||
};
|
||||
this.dragging = true;
|
||||
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
document.addEventListener('mousemove', this.onDragMouseMove);
|
||||
document.addEventListener('mouseup', this.onDragMouseUp);
|
||||
document.addEventListener('contextmenu', this.onContextMenu);
|
||||
},
|
||||
|
||||
/**
|
||||
* 右键菜单弹出时取消拖动
|
||||
* 右键菜单弹出时取消拖动/调整大小
|
||||
*/
|
||||
onContextMenu() {
|
||||
if (this.dragging) {
|
||||
this.onMouseUp();
|
||||
this.onDragMouseUp();
|
||||
}
|
||||
if (this.resizing) {
|
||||
this.onResizeMouseUp();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 鼠标移动
|
||||
* 拖动:鼠标移动
|
||||
*/
|
||||
onMouseMove(e) {
|
||||
onDragMouseMove(e) {
|
||||
if (!this.dragging) return;
|
||||
|
||||
const minMargin = 12;
|
||||
@ -254,17 +297,156 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 鼠标松开
|
||||
* 拖动:鼠标松开
|
||||
*/
|
||||
onMouseUp() {
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
onDragMouseUp() {
|
||||
document.removeEventListener('mousemove', this.onDragMouseMove);
|
||||
document.removeEventListener('mouseup', this.onDragMouseUp);
|
||||
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||
|
||||
this.savePosition();
|
||||
this.dragging = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 调整大小:鼠标按下
|
||||
*/
|
||||
onResizeMouseDown(e, direction) {
|
||||
if (e.button !== 0) return;
|
||||
|
||||
this.updateWindowSize();
|
||||
this.resizeDirection = direction;
|
||||
this.resizeRecord = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startWidth: this.windowSize.width,
|
||||
startHeight: this.windowSize.height,
|
||||
startLeft: this.left,
|
||||
startTop: this.top,
|
||||
};
|
||||
this.resizing = true;
|
||||
|
||||
document.addEventListener('mousemove', this.onResizeMouseMove);
|
||||
document.addEventListener('mouseup', this.onResizeMouseUp);
|
||||
document.addEventListener('contextmenu', this.onContextMenu);
|
||||
},
|
||||
|
||||
/**
|
||||
* 调整大小:鼠标移动
|
||||
*/
|
||||
onResizeMouseMove(e) {
|
||||
if (!this.resizing) return;
|
||||
|
||||
const dir = this.resizeDirection;
|
||||
const deltaX = e.clientX - this.resizeRecord.startX;
|
||||
const deltaY = e.clientY - this.resizeRecord.startY;
|
||||
|
||||
let newWidth = this.resizeRecord.startWidth;
|
||||
let newHeight = this.resizeRecord.startHeight;
|
||||
let newLeft = this.resizeRecord.startLeft;
|
||||
let newTop = this.resizeRecord.startTop;
|
||||
|
||||
// 根据方向计算新尺寸
|
||||
if (dir.includes('e')) {
|
||||
newWidth = this.resizeRecord.startWidth + deltaX;
|
||||
}
|
||||
if (dir.includes('w')) {
|
||||
newWidth = this.resizeRecord.startWidth - deltaX;
|
||||
newLeft = this.resizeRecord.startLeft + deltaX;
|
||||
}
|
||||
if (dir.includes('s')) {
|
||||
newHeight = this.resizeRecord.startHeight + deltaY;
|
||||
}
|
||||
if (dir.includes('n')) {
|
||||
newHeight = this.resizeRecord.startHeight - deltaY;
|
||||
newTop = this.resizeRecord.startTop + deltaY;
|
||||
}
|
||||
|
||||
// 限制最小/最大尺寸
|
||||
const minMargin = 12;
|
||||
const maxWidth = Math.min(this.maxSize.width, this.clientWidth - minMargin * 2);
|
||||
const maxHeight = Math.min(this.maxSize.height, this.clientHeight - minMargin * 2);
|
||||
|
||||
newWidth = Math.max(this.minSize.width, Math.min(newWidth, maxWidth));
|
||||
newHeight = Math.max(this.minSize.height, Math.min(newHeight, maxHeight));
|
||||
|
||||
// 如果是从左边或上边调整,需要修正位置
|
||||
if (dir.includes('w')) {
|
||||
const widthDiff = newWidth - this.resizeRecord.startWidth;
|
||||
newLeft = this.resizeRecord.startLeft - widthDiff;
|
||||
}
|
||||
if (dir.includes('n')) {
|
||||
const heightDiff = newHeight - this.resizeRecord.startHeight;
|
||||
newTop = this.resizeRecord.startTop - heightDiff;
|
||||
}
|
||||
|
||||
// 边界限制位置
|
||||
newLeft = Math.max(minMargin, Math.min(newLeft, this.clientWidth - newWidth - minMargin));
|
||||
newTop = Math.max(minMargin, Math.min(newTop, this.clientHeight - newHeight - minMargin));
|
||||
|
||||
// 更新尺寸
|
||||
this.customSize.width = newWidth;
|
||||
this.customSize.height = newHeight;
|
||||
this.windowSize.width = newWidth;
|
||||
this.windowSize.height = newHeight;
|
||||
|
||||
// 更新位置
|
||||
this.updatePositionFromCoords(newLeft, newTop);
|
||||
},
|
||||
|
||||
/**
|
||||
* 调整大小:鼠标松开
|
||||
*/
|
||||
onResizeMouseUp() {
|
||||
document.removeEventListener('mousemove', this.onResizeMouseMove);
|
||||
document.removeEventListener('mouseup', this.onResizeMouseUp);
|
||||
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||
|
||||
this.saveSize();
|
||||
this.savePosition();
|
||||
this.resizing = false;
|
||||
this.resizeDirection = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 先加载尺寸,再加载位置(确保位置计算时使用正确的尺寸)
|
||||
*/
|
||||
async loadSizeAndPosition() {
|
||||
await this.loadSize();
|
||||
await this.loadPosition();
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载保存的尺寸
|
||||
*/
|
||||
async loadSize() {
|
||||
try {
|
||||
const saved = await $A.IDBString(this.sizeCacheKey);
|
||||
if (saved) {
|
||||
const size = JSON.parse(saved);
|
||||
if (size && typeof size.width === 'number' && typeof size.height === 'number') {
|
||||
this.customSize = {
|
||||
width: Math.max(this.minSize.width, Math.min(size.width, this.maxSize.width)),
|
||||
height: Math.max(this.minSize.height, Math.min(size.height, this.maxSize.height)),
|
||||
};
|
||||
this.windowSize.width = this.customSize.width;
|
||||
this.windowSize.height = this.customSize.height;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存尺寸
|
||||
*/
|
||||
saveSize() {
|
||||
if (this.customSize.width && this.customSize.height) {
|
||||
$A.IDBSave(this.sizeCacheKey, JSON.stringify(this.customSize));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查边界(仅在加载和窗口变化时调用)
|
||||
*/
|
||||
@ -278,13 +460,29 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 窗口大小改变
|
||||
* 视口尺寸变化
|
||||
*/
|
||||
onResize() {
|
||||
this.$nextTick(() => {
|
||||
this.updateWindowSize();
|
||||
this.checkBounds();
|
||||
});
|
||||
onViewportChange() {
|
||||
this.constrainSizeToScreen();
|
||||
this.checkBounds();
|
||||
},
|
||||
|
||||
/**
|
||||
* 限制尺寸不超出屏幕
|
||||
*/
|
||||
constrainSizeToScreen() {
|
||||
const minMargin = 12;
|
||||
const maxWidth = this.clientWidth - minMargin * 2;
|
||||
const maxHeight = this.clientHeight - minMargin * 2;
|
||||
|
||||
if (this.customSize.width && this.customSize.width > maxWidth) {
|
||||
this.customSize.width = Math.max(this.minSize.width, maxWidth);
|
||||
this.windowSize.width = this.customSize.width;
|
||||
}
|
||||
if (this.customSize.height && this.customSize.height > maxHeight) {
|
||||
this.customSize.height = Math.max(this.minSize.height, maxHeight);
|
||||
this.windowSize.height = this.customSize.height;
|
||||
}
|
||||
},
|
||||
|
||||
onClose() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user