feat(ai-assistant): 支持拖动边缘调整聊天窗口大小

- 添加 8 个方向的调整大小控制点(四边 + 四角)
  - 支持从任意边缘或角落拖动调整窗口尺寸
  - 尺寸自动保存到 IndexedDB,下次打开时恢复
  - 窗口大小限制:最小 380×400,最大 800×900
  - 视口尺寸变化时自动调整窗口大小和位置
This commit is contained in:
kuaifan 2026-01-16 10:24:41 +00:00
parent 87dd07ef23
commit a3caf5ebdf
2 changed files with 307 additions and 45 deletions

View File

@ -1305,10 +1305,6 @@ export default {
.ai-assistant-content { .ai-assistant-content {
display: flex; display: flex;
flex-direction: column; 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-welcome,
.ai-assistant-output { .ai-assistant-output {
@ -1575,13 +1571,12 @@ export default {
.ai-assistant-chat { .ai-assistant-chat {
position: fixed; position: fixed;
right: 24px;
bottom: 24px;
width: 460px; width: 460px;
height: 80vh; height: 600px;
min-width: 380px; min-width: 380px;
max-width: 600px; max-width: 800px;
max-height: 640px; min-height: 400px;
max-height: 900px;
background-color: #ffffff; background-color: #ffffff;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.12);
border-radius: 16px; border-radius: 16px;
@ -1589,6 +1584,72 @@ export default {
display: flex; display: flex;
flex-direction: column; 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 { .ai-assistant-close {
position: absolute; position: absolute;
top: 6px; top: 6px;
@ -1631,14 +1692,10 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
@media (max-height: 650px) {
justify-content: normal;
}
.ai-assistant-welcome-icon { .ai-assistant-welcome-icon {
flex-shrink: 0; flex-shrink: 0;
margin-top: auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -1669,6 +1726,7 @@ export default {
gap: 12px; gap: 12px;
max-width: 100%; max-width: 100%;
padding: 0 8px; padding: 0 8px;
margin-bottom: auto;
} }
.ai-assistant-prompt-card { .ai-assistant-prompt-card {
@ -1735,6 +1793,12 @@ export default {
padding: 0 !important; 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 { body.dark-mode-reverse {

View File

@ -9,10 +9,19 @@
<Icon class="ai-assistant-close" type="ios-close" @click="onClose"/> <Icon class="ai-assistant-close" type="ios-close" @click="onClose"/>
<div <div
class="ai-assistant-drag-handle" class="ai-assistant-drag-handle"
@mousedown.stop.prevent="onMouseDown"> @mousedown.stop.prevent="onDragMouseDown">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<slot></slot> <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> </div>
</transition> </transition>
</div> </div>
@ -64,11 +73,31 @@ export default {
dragging: false, dragging: false,
positionLoaded: false, positionLoaded: false,
cacheKey: 'aiAssistant.chatPosition', cacheKey: 'aiAssistant.chatPosition',
sizeCacheKey: 'aiAssistant.chatSize',
//
windowSize: { windowSize: {
width: 460, width: 460,
height: 640, height: 600,
},
//
customSize: {
width: null,
height: null,
},
//
minSize: {
width: 380,
height: 400,
},
maxSize: {
width: 800,
height: 900,
}, },
record: {}, record: {},
//
resizing: false,
resizeDirection: null,
resizeRecord: {},
}; };
}, },
@ -83,11 +112,11 @@ export default {
}, },
clientWidth() { clientWidth() {
return this.windowWidth || document.documentElement.clientWidth; return this.windowWidth;
}, },
clientHeight() { clientHeight() {
return this.windowHeight || document.documentElement.clientHeight; return this.windowHeight;
}, },
// left // left
@ -112,10 +141,18 @@ export default {
opacity: 0, opacity: 0,
}; };
} }
return { const style = {
left: `${this.left}px`, left: `${this.left}px`,
top: `${this.top}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() { mounted() {
this.loadPosition(); this.loadSizeAndPosition();
window.addEventListener('resize', this.onResize);
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.onResize); document.removeEventListener('mousemove', this.onDragMouseMove);
document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onDragMouseUp);
document.removeEventListener('mouseup', this.onMouseUp); document.removeEventListener('mousemove', this.onResizeMouseMove);
document.removeEventListener('mouseup', this.onResizeMouseUp);
document.removeEventListener('contextmenu', this.onContextMenu); document.removeEventListener('contextmenu', this.onContextMenu);
}, },
@ -206,40 +249,40 @@ export default {
}, },
/** /**
* 鼠标按下 * 拖动鼠标按下
*/ */
onMouseDown(e) { onDragMouseDown(e) {
// //
if (e.button !== 0) return; if (e.button !== 0) return;
this.updateWindowSize(); this.updateWindowSize();
this.record = { this.record = {
time: Date.now(),
startLeft: this.left,
startTop: this.top,
offsetX: e.clientX - this.left, offsetX: e.clientX - this.left,
offsetY: e.clientY - this.top, offsetY: e.clientY - this.top,
}; };
this.dragging = true; this.dragging = true;
document.addEventListener('mousemove', this.onMouseMove); document.addEventListener('mousemove', this.onDragMouseMove);
document.addEventListener('mouseup', this.onMouseUp); document.addEventListener('mouseup', this.onDragMouseUp);
document.addEventListener('contextmenu', this.onContextMenu); document.addEventListener('contextmenu', this.onContextMenu);
}, },
/** /**
* 右键菜单弹出时取消拖动 * 右键菜单弹出时取消拖动/调整大小
*/ */
onContextMenu() { onContextMenu() {
if (this.dragging) { if (this.dragging) {
this.onMouseUp(); this.onDragMouseUp();
}
if (this.resizing) {
this.onResizeMouseUp();
} }
}, },
/** /**
* 鼠标移动 * 拖动鼠标移动
*/ */
onMouseMove(e) { onDragMouseMove(e) {
if (!this.dragging) return; if (!this.dragging) return;
const minMargin = 12; const minMargin = 12;
@ -254,17 +297,156 @@ export default {
}, },
/** /**
* 鼠标松开 * 拖动鼠标松开
*/ */
onMouseUp() { onDragMouseUp() {
document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mousemove', this.onDragMouseMove);
document.removeEventListener('mouseup', this.onMouseUp); document.removeEventListener('mouseup', this.onDragMouseUp);
document.removeEventListener('contextmenu', this.onContextMenu); document.removeEventListener('contextmenu', this.onContextMenu);
this.savePosition(); this.savePosition();
this.dragging = false; 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() { onViewportChange() {
this.$nextTick(() => { this.constrainSizeToScreen();
this.updateWindowSize(); this.checkBounds();
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() { onClose() {