From a3caf5ebdfb4f1191d0e95ee4ba53114c6515086 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Fri, 16 Jan 2026 10:24:41 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai-assistant):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8B=96=E5=8A=A8=E8=BE=B9=E7=BC=98=E8=B0=83=E6=95=B4=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E7=AA=97=E5=8F=A3=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 8 个方向的调整大小控制点(四边 + 四角) - 支持从任意边缘或角落拖动调整窗口尺寸 - 尺寸自动保存到 IndexedDB,下次打开时恢复 - 窗口大小限制:最小 380×400,最大 800×900 - 视口尺寸变化时自动调整窗口大小和位置 --- .../js/components/AIAssistant/index.vue | 92 ++++++- .../js/components/AIAssistant/modal.vue | 260 +++++++++++++++--- 2 files changed, 307 insertions(+), 45 deletions(-) diff --git a/resources/assets/js/components/AIAssistant/index.vue b/resources/assets/js/components/AIAssistant/index.vue index 215166607..68f66933f 100644 --- a/resources/assets/js/components/AIAssistant/index.vue +++ b/resources/assets/js/components/AIAssistant/index.vue @@ -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 { diff --git a/resources/assets/js/components/AIAssistant/modal.vue b/resources/assets/js/components/AIAssistant/modal.vue index 3c245219b..f5b0a21c2 100644 --- a/resources/assets/js/components/AIAssistant/modal.vue +++ b/resources/assets/js/components/AIAssistant/modal.vue @@ -9,10 +9,19 @@
+ @mousedown.stop.prevent="onDragMouseDown">
+ +
+
+
+
+
+
+
+
@@ -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() {