mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-23 02:08:11 +00:00
feat(ai-assistant): 浮动按钮支持拖拽到边缘自动收起
- 拖拽按钮到屏幕边缘(≤12px)松开后自动收起为窄条 - 鼠标悬停窄条时自动展开,离开 1 秒后收起 - 点击收起状态的窄条直接打开 AI 助手 - 收起/展开过渡动画平滑,按钮中心位置保持不变 - 仅在 AI 插件安装后显示浮动按钮
This commit is contained in:
parent
12d6bbea19
commit
53dd9dca0f
@ -2,18 +2,28 @@
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
ref="floatBtn"
|
||||
class="ai-float-button"
|
||||
:style="btnStyle"
|
||||
@mousedown.stop.prevent="onMouseDown">
|
||||
<svg class="no-dark-content" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M385.80516777 713.87417358c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404756l-48.91927648-123.9413531c-18.40341303-46.75969229-55.77360888-84.0359932-102.53330118-102.53330117l-123.94135309-48.91927649c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.8257541s7.79328205-24.13100586 19.62404757-28.82575407l123.94135309-48.91927649c46.75969229-18.40341303 84.0359932-55.77360888 102.53330118-102.53330119l48.91927648-123.94135308c4.69474822-11.83076552 16.05603892-19.62404757 28.8257541-19.62404757s24.13100586 7.79328205 28.82575408 19.62404757l48.91927648 123.94135308c18.40341303 46.75969229 55.77360888 84.0359932 102.53330118 102.53330119l123.94135309 48.91927649c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575407 0 12.76971517-7.79328205 24.13100586-19.62404757 28.8257541l-123.94135309 48.91927649c-46.75969229 18.40341303-84.0359932 55.77360888-102.53330118 102.53330117l-48.91927648 123.9413531c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575408 19.62404756zM177.45224165 390.12433614l50.89107073 20.0935224c62.62794129 24.69437565 112.67395736 74.74039171 137.368333 137.36833299l20.09352239 50.89107073 20.0935224-50.89107073c24.69437565-62.62794129 74.74039171-112.67395736 137.368333-137.36833299l50.89107072-20.0935224-50.89107073-20.09352239c-62.62794129-24.69437565-112.67395736-74.74039171-137.36833299-137.36833301l-20.09352239-50.89107074-20.0935224 50.89107074c-24.69437565 62.62794129-74.74039171 112.67395736-137.368333 137.36833301l-50.89107073 20.09352239zM771.33789183 957.62550131c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404758l-26.6661699-67.6043744c-8.63833672-21.87752672-26.10280012-39.34199011-47.98032684-47.98032684l-67.60437441-26.6661699c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.82575409s7.79328205-24.13100586 19.62404757-28.82575409l67.60437441-26.6661699c21.87752672-8.63833672 39.34199011-26.10280012 47.98032684-47.98032685l26.6661699-67.6043744c4.69474822-11.83076552 16.05603892-19.62404757 28.82575409-19.62404757s24.13100586 7.79328205 28.82575409 19.62404757l26.66616991 67.6043744c8.63833672 21.87752672 26.10280012 39.34199011 47.98032684 47.98032685l67.6043744 26.6661699c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575409s-7.79328205 24.13100586-19.62404757 28.82575409l-67.6043744 26.6661699c-21.87752672 8.63833672-39.34199011 26.10280012-47.98032684 47.98032684l-26.66616991 67.6043744c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575409 19.62404758z m-75.58544639-190.70067281c33.61439727 14.83540438 60.75004201 41.87715415 75.49155143 75.49155143 14.83540438-33.61439727 41.87715415-60.75004201 75.49155142-75.49155143-33.61439727-14.83540438-60.75004201-41.87715415-75.49155142-75.49155143-14.74150942 33.61439727-41.87715415 60.75004201-75.49155143 75.49155143z"/>
|
||||
</svg>
|
||||
class="ai-float-button-wrapper"
|
||||
:class="wrapperClass"
|
||||
:style="wrapperStyle"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave">
|
||||
<div
|
||||
ref="floatBtn"
|
||||
class="ai-float-button"
|
||||
:class="btnClass"
|
||||
:style="btnStyle"
|
||||
@mousedown.stop.prevent="onMouseDown">
|
||||
<!-- 完整图标 -->
|
||||
<svg class="ai-float-button-icon no-dark-content" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M385.80516777 713.87417358c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404756l-48.91927648-123.9413531c-18.40341303-46.75969229-55.77360888-84.0359932-102.53330118-102.53330117l-123.94135309-48.91927649c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.8257541s7.79328205-24.13100586 19.62404757-28.82575407l123.94135309-48.91927649c46.75969229-18.40341303 84.0359932-55.77360888 102.53330118-102.53330119l48.91927648-123.94135308c4.69474822-11.83076552 16.05603892-19.62404757 28.8257541-19.62404757s24.13100586 7.79328205 28.82575408 19.62404757l48.91927648 123.94135308c18.40341303 46.75969229 55.77360888 84.0359932 102.53330118 102.53330119l123.94135309 48.91927649c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575407 0 12.76971517-7.79328205 24.13100586-19.62404757 28.8257541l-123.94135309 48.91927649c-46.75969229 18.40341303-84.0359932 55.77360888-102.53330118 102.53330117l-48.91927648 123.9413531c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575408 19.62404756zM177.45224165 390.12433614l50.89107073 20.0935224c62.62794129 24.69437565 112.67395736 74.74039171 137.368333 137.36833299l20.09352239 50.89107073 20.0935224-50.89107073c24.69437565-62.62794129 74.74039171-112.67395736 137.368333-137.36833299l50.89107072-20.0935224-50.89107073-20.09352239c-62.62794129-24.69437565-112.67395736-74.74039171-137.36833299-137.36833301l-20.09352239-50.89107074-20.0935224 50.89107074c-24.69437565 62.62794129-74.74039171 112.67395736-137.368333 137.36833301l-50.89107073 20.09352239zM771.33789183 957.62550131c-12.76971517 0-24.13100586-7.79328205-28.82575409-19.62404758l-26.6661699-67.6043744c-8.63833672-21.87752672-26.10280012-39.34199011-47.98032684-47.98032684l-67.60437441-26.6661699c-11.83076552-4.69474822-19.62404757-16.05603892-19.62404757-28.82575409s7.79328205-24.13100586 19.62404757-28.82575409l67.60437441-26.6661699c21.87752672-8.63833672 39.34199011-26.10280012 47.98032684-47.98032685l26.6661699-67.6043744c4.69474822-11.83076552 16.05603892-19.62404757 28.82575409-19.62404757s24.13100586 7.79328205 28.82575409 19.62404757l26.66616991 67.6043744c8.63833672 21.87752672 26.10280012 39.34199011 47.98032684 47.98032685l67.6043744 26.6661699c11.83076552 4.69474822 19.62404757 16.05603892 19.62404757 28.82575409s-7.79328205 24.13100586-19.62404757 28.82575409l-67.6043744 26.6661699c-21.87752672 8.63833672-39.34199011 26.10280012-47.98032684 47.98032684l-26.66616991 67.6043744c-4.69474822 11.83076552-16.14993388 19.62404757-28.82575409 19.62404758z m-75.58544639-190.70067281c33.61439727 14.83540438 60.75004201 41.87715415 75.49155143 75.49155143 14.83540438-33.61439727 41.87715415-60.75004201 75.49155142-75.49155143-33.61439727-14.83540438-60.75004201-41.87715415-75.49155142-75.49155143-14.74150942 33.61439727-41.87715415 60.75004201-75.49155143 75.49155143z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
import emitter from "../../store/events";
|
||||
import {withLanguagePreferencePrompt} from "../../utils/ai";
|
||||
import {getPageContext} from "./page-context";
|
||||
@ -29,47 +39,38 @@ export default {
|
||||
y: 24, // 垂直距离值
|
||||
fromRight: true, // true: 距右边, false: 距左边
|
||||
fromBottom: true, // true: 距底部, false: 距顶部
|
||||
collapsed: false, // 是否收起到边缘
|
||||
},
|
||||
dragging: false,
|
||||
positionLoaded: false,
|
||||
cacheKey: 'aiAssistant.floatButtonPosition',
|
||||
btnSize: 44,
|
||||
collapsedHeight: 48, // 收起时的高度
|
||||
collapseThreshold: 12, // 触发收起的边缘距离阈值
|
||||
collapseDelay: 1000, // 收起延迟(毫秒)
|
||||
collapseTimer: null, // 收起定时器
|
||||
record: {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['microAppsIds']),
|
||||
|
||||
aiInstalled() {
|
||||
return this.microAppsIds?.includes('ai');
|
||||
},
|
||||
|
||||
visible() {
|
||||
return this.userId > 0 &&
|
||||
return this.aiInstalled &&
|
||||
this.userId > 0 &&
|
||||
this.positionLoaded &&
|
||||
!this.windowPortrait &&
|
||||
this.routeName !== 'login' &&
|
||||
!this.$parent?.showModal;
|
||||
},
|
||||
|
||||
// 计算实际的 left 值
|
||||
left() {
|
||||
if (this.position.fromRight) {
|
||||
return this.clientWidth - this.btnSize - this.position.x;
|
||||
}
|
||||
return this.position.x;
|
||||
},
|
||||
|
||||
// 计算实际的 top 值
|
||||
top() {
|
||||
if (this.position.fromBottom) {
|
||||
return this.clientHeight - this.btnSize - this.position.y;
|
||||
}
|
||||
return this.position.y;
|
||||
},
|
||||
|
||||
btnStyle() {
|
||||
return {
|
||||
left: `${this.left}px`,
|
||||
top: `${this.top}px`,
|
||||
width: `${this.btnSize}px`,
|
||||
height: `${this.btnSize}px`,
|
||||
};
|
||||
collapsed() {
|
||||
return this.position.collapsed;
|
||||
},
|
||||
|
||||
clientWidth() {
|
||||
@ -79,6 +80,67 @@ export default {
|
||||
clientHeight() {
|
||||
return this.windowHeight || document.documentElement.clientHeight;
|
||||
},
|
||||
|
||||
// wrapper 宽度:包含按钮所有可能位置(边缘到 position.x 的距离)
|
||||
wrapperWidth() {
|
||||
return this.btnSize + this.position.x;
|
||||
},
|
||||
|
||||
// wrapper 左边位置
|
||||
wrapperLeft() {
|
||||
if (this.position.fromRight) {
|
||||
// 右侧:从 (屏幕宽 - wrapper宽度) 开始
|
||||
return this.clientWidth - this.wrapperWidth;
|
||||
}
|
||||
// 左侧:从 0 开始
|
||||
return 0;
|
||||
},
|
||||
|
||||
wrapperTop() {
|
||||
// 基于按钮中心计算,确保收起/展开时中心位置不变
|
||||
const centerY = this.position.fromBottom
|
||||
? this.clientHeight - this.btnSize / 2 - this.position.y
|
||||
: this.position.y + this.btnSize / 2;
|
||||
return centerY - this.wrapperHeight / 2;
|
||||
},
|
||||
|
||||
wrapperClass() {
|
||||
return {
|
||||
'is-left': !this.position.fromRight,
|
||||
'is-right': this.position.fromRight,
|
||||
'is-dragging': this.dragging,
|
||||
'is-collapsed': this.collapsed,
|
||||
};
|
||||
},
|
||||
|
||||
wrapperHeight() {
|
||||
return this.collapsed ? this.collapsedHeight : this.btnSize;
|
||||
},
|
||||
|
||||
wrapperStyle() {
|
||||
return {
|
||||
left: `${this.wrapperLeft}px`,
|
||||
top: `${this.wrapperTop}px`,
|
||||
width: `${this.wrapperWidth}px`,
|
||||
height: `${this.wrapperHeight}px`,
|
||||
};
|
||||
},
|
||||
|
||||
btnClass() {
|
||||
return {
|
||||
'is-collapsed': this.collapsed,
|
||||
};
|
||||
},
|
||||
|
||||
btnStyle() {
|
||||
// 收起时按钮在边缘(不偏移),展开时向内偏移 position.x
|
||||
if (this.collapsed) {
|
||||
return { transform: 'translateX(0)' };
|
||||
}
|
||||
// 右侧向左偏移(负值),左侧向右偏移(正值)
|
||||
const offset = this.position.fromRight ? -this.position.x : this.position.x;
|
||||
return { transform: `translateX(${offset}px)` };
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@ -91,6 +153,7 @@ export default {
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||
this.clearCollapseTimer();
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -103,7 +166,8 @@ export default {
|
||||
if (saved) {
|
||||
const pos = JSON.parse(saved);
|
||||
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
|
||||
this.position = pos;
|
||||
// 兼容旧格式,确保有 collapsed 字段
|
||||
this.position = {...pos, collapsed: pos.collapsed ?? false};
|
||||
this.$nextTick(() => {
|
||||
this.checkBounds();
|
||||
this.positionLoaded = true;
|
||||
@ -115,7 +179,7 @@ export default {
|
||||
// ignore
|
||||
}
|
||||
// 默认位置:右下角
|
||||
this.position = {x: 24, y: 24, fromRight: true, fromBottom: true};
|
||||
this.position = {x: 24, y: 24, fromRight: true, fromBottom: true, collapsed: false};
|
||||
this.positionLoaded = true;
|
||||
},
|
||||
|
||||
@ -141,7 +205,7 @@ export default {
|
||||
const x = fromRight ? (this.clientWidth - this.btnSize - left) : left;
|
||||
const y = fromBottom ? (this.clientHeight - this.btnSize - top) : top;
|
||||
|
||||
this.position = {x, y, fromRight, fromBottom};
|
||||
this.position = {x, y, fromRight, fromBottom, collapsed: this.position.collapsed};
|
||||
},
|
||||
|
||||
/**
|
||||
@ -151,12 +215,20 @@ export default {
|
||||
// 只响应鼠标左键
|
||||
if (e.button !== 0) return;
|
||||
|
||||
// 收起状态下点击直接打开 AI 助手
|
||||
if (this.collapsed) {
|
||||
this.onClick();
|
||||
return;
|
||||
}
|
||||
|
||||
// 按钮的实际视觉位置
|
||||
const btnRect = this.$refs.floatBtn.getBoundingClientRect();
|
||||
this.record = {
|
||||
time: Date.now(),
|
||||
startLeft: this.left,
|
||||
startTop: this.top,
|
||||
offsetX: e.clientX - this.left,
|
||||
offsetY: e.clientY - this.top,
|
||||
startLeft: btnRect.left,
|
||||
startTop: btnRect.top,
|
||||
offsetX: e.clientX - btnRect.left,
|
||||
offsetY: e.clientY - btnRect.top,
|
||||
};
|
||||
this.dragging = true;
|
||||
|
||||
@ -199,7 +271,8 @@ export default {
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
document.removeEventListener('contextmenu', this.onContextMenu);
|
||||
|
||||
const moveDistance = Math.abs(this.left - this.record.startLeft) + Math.abs(this.top - this.record.startTop);
|
||||
const btnRect = this.$refs.floatBtn.getBoundingClientRect();
|
||||
const moveDistance = Math.abs(btnRect.left - this.record.startLeft) + Math.abs(btnRect.top - this.record.startTop);
|
||||
const duration = Date.now() - this.record.time;
|
||||
|
||||
this.savePosition();
|
||||
@ -209,6 +282,55 @@ export default {
|
||||
if (moveDistance < 5 && duration < 200) {
|
||||
this.onClick();
|
||||
}
|
||||
|
||||
// 检测是否靠近左右边缘,延迟收起
|
||||
this.scheduleCollapse();
|
||||
},
|
||||
|
||||
/**
|
||||
* 鼠标进入
|
||||
*/
|
||||
onMouseEnter() {
|
||||
this.clearCollapseTimer();
|
||||
// 如果是收起状态,展开
|
||||
if (this.collapsed) {
|
||||
this.position.collapsed = false;
|
||||
this.savePosition();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 鼠标离开
|
||||
*/
|
||||
onMouseLeave() {
|
||||
// 拖拽期间不处理
|
||||
if (this.dragging) return;
|
||||
// 延迟检测是否需要收起
|
||||
this.scheduleCollapse();
|
||||
},
|
||||
|
||||
/**
|
||||
* 计划收起(延迟执行)
|
||||
*/
|
||||
scheduleCollapse() {
|
||||
this.clearCollapseTimer();
|
||||
// 检测是否靠近边缘
|
||||
if (this.position.x <= this.collapseThreshold) {
|
||||
this.collapseTimer = setTimeout(() => {
|
||||
this.position.collapsed = true;
|
||||
this.savePosition();
|
||||
}, this.collapseDelay);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除收起定时器
|
||||
*/
|
||||
clearCollapseTimer() {
|
||||
if (this.collapseTimer) {
|
||||
clearTimeout(this.collapseTimer);
|
||||
this.collapseTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -270,9 +392,36 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ai-float-button {
|
||||
$btn-size: 44px;
|
||||
$collapsed-width: 12px;
|
||||
$collapsed-height: 48px;
|
||||
|
||||
.ai-float-button-wrapper {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// 右侧:按钮默认在右边(屏幕边缘),展开时通过 transform 向左移动
|
||||
&.is-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// 左侧:按钮默认在左边(屏幕边缘),展开时通过 transform 向右移动
|
||||
&.is-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
.ai-float-button {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-float-button {
|
||||
width: $btn-size;
|
||||
height: $btn-size;
|
||||
border-radius: 50%;
|
||||
background: #8bcf70;
|
||||
box-shadow: 0 4px 12px lch(77 53.3 131.54 / 0.4);
|
||||
@ -280,24 +429,40 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
transition: transform 0.25s ease-out, box-shadow 0.2s, width 0.25s ease-out, height 0.25s ease-out, border-radius 0.25s ease-out;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
&:not(.is-collapsed):hover {
|
||||
box-shadow: 0 6px 16px lch(77 53.3 131.54 / 0.5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
svg {
|
||||
.ai-float-button-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
// 收起状态
|
||||
&.is-collapsed {
|
||||
width: $collapsed-width;
|
||||
height: $collapsed-height;
|
||||
box-shadow: 0 2px 8px lch(77 53.3 131.54 / 0.3);
|
||||
|
||||
.ai-float-button-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-left &.is-collapsed {
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.is-right &.is-collapsed {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
body.dark-mode-reverse {
|
||||
.ai-float-button {
|
||||
box-shadow: none;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user