Files
YouduWiki/web/app/public/widget-bot.js
2026-05-21 19:52:45 +08:00

951 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
'use strict';
const defaultModalPosition = 'follow';
const defaultBtnPosition = 'bottom_right';
const defaultBtnStyle = 'side_sticky';
// 获取当前脚本的域名
const currentScript = document.currentScript || document.querySelector('script[src*="widget-bot.js"]');
const ulrObj = new URL(currentScript.src)
const widgetDomain = `${ulrObj.origin}${ulrObj.pathname}`.replace('/widget-bot.js', '')
let widgetInfo = null;
let widgetButton = null;
let widgetModal = null;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let currentTheme = 'light'; // 默认浅色主题
let customTriggerElement = null; // 自定义触发元素
let customTriggerHandler = null; // 自定义触发元素的事件处理函数
let dragAnimationFrame = null; // 拖拽动画帧ID
let buttonSize = { width: 0, height: 0 }; // 缓存按钮尺寸
let initialPosition = { left: 0, top: 0 }; // 拖拽开始时的初始位置
let hasDragged = false; // 标记是否发生了拖拽
let dragStartPos = { x: 0, y: 0 }; // 拖拽开始时的鼠标位置
// 应用主题
function applyTheme(theme_mode) {
currentTheme = theme_mode === 'dark' ? 'dark' : 'light';
updateThemeClasses();
}
// 更新主题类名
function updateThemeClasses() {
if (widgetButton) {
widgetButton.setAttribute('data-theme', currentTheme);
}
if (widgetModal) {
widgetModal.setAttribute('data-theme', currentTheme);
}
}
// 获取挂件信息
async function fetchWidgetInfo() {
if (widgetButton) {
widgetButton.classList.add('loading');
}
try {
const response = await fetch(`${widgetDomain}/share/v1/app/widget/info`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
widgetInfo = data.data.settings?.widget_bot_settings;
// 验证返回的数据结构
if (!widgetInfo || typeof widgetInfo !== 'object') {
throw new Error('Invalid widget info response');
}
// 应用主题模式
if (widgetInfo.theme_mode) {
applyTheme(widgetInfo.theme_mode);
}
// 根据 btn_style 创建不同的挂件
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
if (btnStyle === 'btn_trigger') {
createCustomTrigger();
} else {
createWidget();
}
} catch (error) {
console.error('获取挂件信息失败:', error);
// 使用默认值
widgetInfo = {
btn_text: '在线客服',
btn_logo: `''`,
btn_style: defaultBtnStyle,
btn_position: defaultBtnPosition,
modal_position: defaultModalPosition,
theme_mode: 'light'
};
applyTheme(widgetInfo.theme_mode);
createWidget();
} finally {
if (widgetButton) {
widgetButton.classList.remove('loading');
}
}
}
// 应用按钮位置
function applyButtonPosition(button, position) {
const pos = position || defaultBtnPosition;
button.style.top = 'auto';
button.style.right = 'auto';
button.style.bottom = 'auto';
button.style.left = 'auto';
// 两种模式使用相同的默认位置距离边缘16px垂直方向190px
switch (pos) {
case 'top_left':
button.style.top = '190px';
button.style.left = '16px';
break;
case 'top_right':
button.style.top = '190px';
button.style.right = '16px';
break;
case 'bottom_left':
button.style.bottom = '190px';
button.style.left = '16px';
break;
case 'bottom_right':
default:
button.style.bottom = '190px';
button.style.right = '16px';
break;
}
}
// 创建侧边吸附按钮
function createSideStickyButton() {
widgetButton = document.createElement('div');
widgetButton.className = 'widget-bot-button widget-bot-side-sticky';
widgetButton.setAttribute('role', 'button');
widgetButton.setAttribute('tabindex', '0');
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
widgetButton.setAttribute('data-theme', currentTheme);
const buttonContent = document.createElement('div');
buttonContent.className = 'widget-bot-button-content';
// 侧边吸附显示图标和文字btn_logo 以及 btn_text
const icon = document.createElement('img');
const defaultIconSrc = widgetDomain + '/favicon.png';
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
icon.alt = 'icon';
icon.className = 'widget-bot-icon';
icon.onerror = () => {
// 如果当前不是 favicon.png尝试使用 favicon.png 作为备用
if (icon.src !== defaultIconSrc) {
icon.src = defaultIconSrc;
} else {
// 如果 favicon.png 也加载失败,隐藏图标
icon.style.display = 'none';
}
};
buttonContent.appendChild(icon);
// 添加文字
const textDiv = document.createElement('div');
textDiv.className = 'widget-bot-text';
textDiv.textContent = widgetInfo.btn_text || '在线客服';
// 设置固定宽度、自动换行和居中
textDiv.style.wordWrap = 'break-word';
textDiv.style.whiteSpace = 'normal';
textDiv.style.textAlign = 'center';
buttonContent.appendChild(textDiv);
widgetButton.appendChild(buttonContent);
// 应用位置 - 距离边缘16px垂直方向190px
const position = widgetInfo.btn_position || defaultBtnPosition;
applyButtonPosition(widgetButton, position);
// 设置 border-radius 为 24px统一圆角
widgetButton.style.borderRadius = '24px';
// 添加事件监听器
widgetButton.addEventListener('click', handleButtonClick);
widgetButton.addEventListener('mousedown', startDrag);
widgetButton.addEventListener('keydown', handleKeyDown);
// 添加触摸事件支持
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
widgetButton.addEventListener('touchend', handleTouchEnd);
document.body.appendChild(widgetButton);
}
// 创建悬浮球按钮
function createHoverBallButton() {
widgetButton = document.createElement('div');
widgetButton.className = 'widget-bot-button widget-bot-hover-ball';
widgetButton.setAttribute('role', 'button');
widgetButton.setAttribute('tabindex', '0');
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
widgetButton.setAttribute('data-theme', currentTheme);
const buttonContent = document.createElement('div');
buttonContent.className = 'widget-bot-button-content';
// 悬浮球只显示图标btn_logo
const icon = document.createElement('img');
const defaultIconSrc = widgetDomain + '/favicon.png';
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
icon.alt = 'icon';
icon.className = 'widget-bot-icon widget-bot-hover-ball-icon';
icon.onerror = () => {
// 如果当前不是 favicon.png尝试使用 favicon.png 作为备用
if (icon.src !== defaultIconSrc) {
icon.src = defaultIconSrc;
} else {
// 如果 favicon.png 也加载失败,隐藏图标
icon.style.display = 'none';
}
};
buttonContent.appendChild(icon);
widgetButton.appendChild(buttonContent);
// 应用位置 - 距离边缘16px垂直方向190px
applyButtonPosition(widgetButton, widgetInfo.btn_position || defaultBtnPosition);
// 添加事件监听器
widgetButton.addEventListener('click', handleButtonClick);
widgetButton.addEventListener('mousedown', startDrag);
widgetButton.addEventListener('keydown', handleKeyDown);
// 添加触摸事件支持
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
widgetButton.addEventListener('touchend', handleTouchEnd);
document.body.appendChild(widgetButton);
}
// 创建挂件按钮
function createWidget() {
// 如果已存在,先删除
if (widgetButton) {
widgetButton.remove();
}
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
if (btnStyle === 'hover_ball') {
createHoverBallButton();
} else {
createSideStickyButton();
}
// 创建模态框
createModal();
// 触发显示动画
setTimeout(() => {
widgetButton.style.opacity = '1';
}, 100);
}
// 创建自定义触发按钮
function createCustomTrigger() {
const btnId = widgetInfo.btn_id;
if (!btnId) {
console.error('btn_trigger 模式需要提供 btn_id');
return;
}
let retryCount = 0;
const maxRetries = 50; // 最多重试 50 次5秒
// 绑定事件到元素
function attachTrigger(element) {
if (!element) return;
// 避免重复绑定
if (element.hasAttribute('data-widget-trigger-attached')) {
return;
}
element.setAttribute('data-widget-trigger-attached', 'true');
customTriggerElement = element;
// 创建事件处理函数并保存引用
customTriggerHandler = function (e) {
e.preventDefault();
e.stopPropagation();
showModal();
};
// 绑定点击事件
element.addEventListener('click', customTriggerHandler);
}
// 尝试查找并绑定元素
function tryAttachTrigger() {
const element = document.getElementById(btnId);
if (element) {
attachTrigger(element);
createModal();
return true;
}
return false;
}
// 立即尝试一次
if (tryAttachTrigger()) {
return;
}
// 如果元素还没加载,使用多种方式监听
function retryAttach() {
if (tryAttachTrigger()) {
return;
}
retryCount++;
if (retryCount < maxRetries) {
setTimeout(retryAttach, 100);
} else {
console.warn('自定义触发按钮未找到,已停止重试:', btnId);
}
}
// 使用 MutationObserver 监听 DOM 变化
const observer = new MutationObserver(function (mutations) {
if (tryAttachTrigger()) {
observer.disconnect();
}
});
// 开始观察 DOM 变化
observer.observe(document.body, {
childList: true,
subtree: true
});
// 如果 DOM 已加载完成,立即开始重试
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
setTimeout(retryAttach, 100);
});
} else {
setTimeout(retryAttach, 100);
}
// 延迟断开观察器(避免无限观察)
setTimeout(function () {
observer.disconnect();
}, 10000); // 10秒后断开
}
// 处理按钮点击事件(区分点击和拖拽)
function handleButtonClick(e) {
// 如果发生了拖拽,不打开弹框
if (hasDragged) {
e.preventDefault();
e.stopPropagation();
return;
}
showModal();
}
// 键盘事件处理
function handleKeyDown(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
showModal();
}
}
// 触摸事件处理
let touchStartPos = { x: 0, y: 0 };
function handleTouchStart(e) {
const touch = e.touches[0];
touchStartPos = { x: touch.clientX, y: touch.clientY };
startDrag(e);
}
function handleTouchMove(e) {
if (!isDragging) return;
e.preventDefault()
const touch = e.touches[0];
drag({ clientX: touch.clientX, clientY: touch.clientY });
}
function handleTouchEnd(e) {
const touch = e.changedTouches[0];
const distance = Math.sqrt(
Math.pow(touch.clientX - touchStartPos.x, 2) +
Math.pow(touch.clientY - touchStartPos.y, 2)
);
// 只有在没有拖拽且移动距离很小的情况下才认为是点击
if (!hasDragged && distance < 10) {
// 判断为点击事件
setTimeout(() => showModal(), 100);
}
stopDrag();
}
// 创建模态框
function createModal() {
// 如果已存在,先删除
if (widgetModal) {
widgetModal.remove();
}
widgetModal = document.createElement('div');
widgetModal.className = 'widget-bot-modal';
widgetModal.setAttribute('role', 'dialog');
widgetModal.setAttribute('aria-modal', 'true');
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
widgetModal.setAttribute('data-theme', currentTheme);
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
if (modalPosition === 'fixed') {
widgetModal.classList.add('widget-bot-modal-fixed');
}
const modalContent = document.createElement('div');
modalContent.className = 'widget-bot-modal-content';
if (modalPosition === 'fixed') {
modalContent.classList.add('widget-bot-modal-content-fixed');
}
// 创建关闭按钮(透明框)
const closeBtn = document.createElement('button');
closeBtn.className = 'widget-bot-close-btn';
closeBtn.setAttribute('aria-label', '关闭窗口');
closeBtn.setAttribute('type', 'button');
// 创建一个内部元素来处理实际的点击事件(因为按钮设置了 pointer-events: none
const closeBtnArea = document.createElement('div');
closeBtnArea.style.width = '100%';
closeBtnArea.style.height = '100%';
closeBtnArea.style.pointerEvents = 'auto'; // 内部元素可以接收事件
closeBtnArea.style.cursor = 'pointer';
closeBtnArea.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
hideModal();
});
closeBtn.appendChild(closeBtnArea);
// 创建iframe
const iframe = document.createElement('iframe');
iframe.className = 'widget-bot-iframe';
iframe.src = `${widgetDomain}/widget`;
iframe.setAttribute('title', `${widgetInfo.btn_text || '在线客服'}服务窗口`);
iframe.setAttribute('allow', 'camera; microphone; geolocation');
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
modalContent.appendChild(closeBtn);
modalContent.appendChild(iframe);
widgetModal.appendChild(modalContent);
document.body.appendChild(widgetModal);
}
// 检测是否为移动端
function isMobile() {
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// 智能定位弹框follow模式
function positionModalFollow(modalContent) {
if (!widgetButton || !modalContent) return;
// 移动端强制居中显示
if (isMobile()) {
modalContent.style.position = 'relative';
modalContent.style.top = 'auto';
modalContent.style.left = 'auto';
modalContent.style.right = 'auto';
modalContent.style.bottom = 'auto';
modalContent.style.margin = 'auto';
modalContent.style.width = 'calc(100% - 32px)';
modalContent.style.height = 'auto';
return;
}
requestAnimationFrame(() => {
const buttonRect = widgetButton.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const margin = 16; // 距离屏幕边缘的最小距离
const buttonGap = 16; // 弹框和按钮之间的最小距离
// 先设置一个临时位置来获取弹框尺寸
const originalPosition = modalContent.style.position;
const originalTop = modalContent.style.top;
const originalLeft = modalContent.style.left;
const originalVisibility = modalContent.style.visibility;
const originalDisplay = modalContent.style.display;
modalContent.style.position = 'absolute';
modalContent.style.top = '0';
modalContent.style.left = '0';
modalContent.style.visibility = 'hidden';
modalContent.style.display = 'block';
const modalRect = modalContent.getBoundingClientRect();
const modalWidth = modalRect.width;
const modalHeight = modalRect.height;
modalContent.style.visibility = originalVisibility || 'visible';
modalContent.style.display = originalDisplay || 'block';
// 计算按钮中心点
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
// 判断按钮在屏幕的哪一侧
const isLeftSide = buttonCenterX < windowWidth / 2;
const isTopSide = buttonCenterY < windowHeight / 2;
// 智能选择弹框位置,确保完整显示
let finalTop, finalBottom, finalLeft, finalRight;
if (isLeftSide) {
// 按钮在左侧,弹框优先显示在右侧(按钮右侧)
finalLeft = buttonRect.right + buttonGap;
finalRight = 'auto';
// 如果右侧空间不够,显示在左侧(按钮左侧)
if (finalLeft + modalWidth > windowWidth - margin) {
finalLeft = 'auto';
finalRight = windowWidth - buttonRect.left + buttonGap;
// 如果左侧空间也不够,则贴左边(但保持与按钮的距离)
if (buttonRect.left - buttonGap - modalWidth < margin) {
finalLeft = margin;
finalRight = 'auto';
}
}
} else {
// 按钮在右侧,弹框优先显示在左侧(按钮左侧)
finalLeft = 'auto';
finalRight = windowWidth - buttonRect.left + buttonGap;
// 如果左侧空间不够,显示在右侧(按钮右侧)
if (buttonRect.left - buttonGap - modalWidth < margin) {
finalRight = 'auto';
finalLeft = buttonRect.right + buttonGap;
// 如果右侧空间也不够,则贴右边(但保持与按钮的距离)
if (finalLeft + modalWidth > windowWidth - margin) {
finalLeft = 'auto';
finalRight = margin;
}
}
}
// 垂直方向:优先与按钮顶部对齐
// 弹框顶部与按钮顶部对齐
finalTop = buttonRect.top;
finalBottom = 'auto';
// 如果弹框底部超出屏幕,则向上调整,确保弹框完整显示在屏幕内
if (finalTop + modalHeight > windowHeight - margin) {
// 计算向上调整后的位置
const adjustedTop = windowHeight - margin - modalHeight;
// 如果调整后的位置仍然在按钮上方,则使用调整后的位置
if (adjustedTop >= margin) {
finalTop = adjustedTop;
} else {
// 如果调整后仍然超出,则贴顶部
finalTop = margin;
}
} else if (finalTop < margin) {
// 如果弹框顶部超出屏幕,则贴顶部
finalTop = margin;
}
// 应用最终位置
modalContent.style.top = finalTop !== undefined ? (typeof finalTop === 'string' ? finalTop : finalTop + 'px') : 'auto';
modalContent.style.bottom = finalBottom !== undefined ? (typeof finalBottom === 'string' ? finalBottom : finalBottom + 'px') : 'auto';
modalContent.style.left = finalLeft !== undefined ? (typeof finalLeft === 'string' ? finalLeft : finalLeft + 'px') : 'auto';
modalContent.style.right = finalRight !== undefined ? (typeof finalRight === 'string' ? finalRight : finalRight + 'px') : 'auto';
// 最终检查并修正,确保弹框完全在屏幕内
requestAnimationFrame(() => {
const finalModalRect = modalContent.getBoundingClientRect();
// 修正左边界
if (finalModalRect.left < margin) {
modalContent.style.left = margin + 'px';
modalContent.style.right = 'auto';
}
// 修正右边界
if (finalModalRect.right > windowWidth - margin) {
modalContent.style.right = margin + 'px';
modalContent.style.left = 'auto';
}
// 修正上边界
if (finalModalRect.top < margin) {
modalContent.style.top = margin + 'px';
modalContent.style.bottom = 'auto';
}
// 修正下边界
if (finalModalRect.bottom > windowHeight - margin) {
modalContent.style.bottom = margin + 'px';
modalContent.style.top = 'auto';
}
});
});
}
// 显示模态框
function showModal() {
if (!widgetModal) return;
widgetModal.style.display = 'flex';
document.body.classList.add('widget-bot-modal-open');
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
// 移动端强制居中显示
if (isMobile()) {
modalContent.style.position = 'relative';
modalContent.style.top = 'auto';
modalContent.style.left = 'auto';
modalContent.style.right = 'auto';
modalContent.style.bottom = 'auto';
modalContent.style.margin = 'auto';
modalContent.style.width = 'calc(100% - 32px)';
modalContent.style.height = 'auto';
} else if (modalPosition === 'fixed') {
// 桌面端固定模式:居中展示
modalContent.style.position = 'relative';
modalContent.style.top = 'auto';
modalContent.style.left = 'auto';
modalContent.style.right = 'auto';
modalContent.style.bottom = 'auto';
modalContent.style.margin = 'auto';
} else {
// 桌面端跟随模式:跟随按钮位置 - 智能定位,确保弹框完整显示在屏幕内
positionModalFollow(modalContent);
}
// 添加ESC键关闭功能先移除避免重复绑定
document.removeEventListener('keydown', handleEscKey);
document.addEventListener('keydown', handleEscKey);
}
// ESC键处理
function handleEscKey(e) {
// 只在弹框显示时响应 ESC 键
if (e.key === 'Escape' && widgetModal && widgetModal.style.display === 'flex') {
hideModal();
}
}
// 隐藏模态框
function hideModal() {
if (!widgetModal) return;
widgetModal.style.display = 'none';
document.body.classList.remove('widget-bot-modal-open');
// 恢复焦点到按钮
if (widgetButton) {
widgetButton.focus();
}
// 移除ESC键监听
document.removeEventListener('keydown', handleEscKey);
}
// 开始拖拽
function startDrag(e) {
if (e.preventDefault) {
e.preventDefault()
};
isDragging = true;
hasDragged = false; // 重置拖拽标记
const rect = widgetButton.getBoundingClientRect();
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
// 记录拖拽开始位置
dragStartPos.x = clientX;
dragStartPos.y = clientY;
// 由于 transform-origin 是 centerscale 不会改变元素中心位置
// 但 getBoundingClientRect() 返回的尺寸是放大后的,需要计算原始尺寸
// 假设当前可能有 scale(1.1),计算原始尺寸
const scale = 1.1; // hover 时的 scale 值
const originalWidth = rect.width / scale;
const originalHeight = rect.height / scale;
// 缓存按钮原始尺寸(未缩放)
buttonSize.width = originalWidth;
buttonSize.height = originalHeight;
// 由于 transform-origin 是 center元素的左上角位置需要考虑 scale 的影响
// 中心点位置不变,但左上角会向左上移动
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const originalLeft = centerX - originalWidth / 2;
const originalTop = centerY - originalHeight / 2;
initialPosition.left = originalLeft;
initialPosition.top = originalTop;
// 计算鼠标相对于原始尺寸(未缩放)按钮左上角的偏移
dragOffset.x = clientX - originalLeft;
dragOffset.y = clientY - originalTop;
widgetButton.style.position = 'fixed';
widgetButton.style.top = originalTop + 'px';
widgetButton.style.left = originalLeft + 'px';
widgetButton.style.right = 'auto';
widgetButton.style.bottom = 'auto';
// 保持 scale 效果
widgetButton.style.transform = 'scale(1.1)';
widgetButton.style.transition = 'none';
widgetButton.style.willChange = 'left, top, transform';
document.addEventListener('mousemove', drag, { passive: false });
document.addEventListener('mouseup', stopDrag);
widgetButton.classList.add('dragging');
widgetButton.style.zIndex = '10001';
}
// 拖拽中 - 直接更新位置,实现丝滑跟随
function drag(e) {
if (!isDragging) return;
if (e.preventDefault) {
e.preventDefault();
}
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
// 检测是否发生了实际移动超过5px才认为是拖拽
const moveDistance = Math.sqrt(
Math.pow(clientX - dragStartPos.x, 2) +
Math.pow(clientY - dragStartPos.y, 2)
);
if (moveDistance > 5) {
hasDragged = true;
}
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const buttonWidth = buttonSize.width;
const buttonHeight = buttonSize.height;
// 直接基于鼠标位置计算新位置
// 鼠标位置减去拖拽偏移量,得到按钮左上角应该的位置
const newLeft = clientX - dragOffset.x;
const newTop = clientY - dragOffset.y;
// 垂直位置:限制在屏幕范围内,距离顶部和底部最小距离为 24px
const minTop = 24;
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
const constrainedTop = Math.max(minTop, Math.min(newTop, maxTop));
// 水平位置:限制在屏幕范围内
const maxLeft = windowWidth - buttonWidth;
const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft));
widgetButton.style.left = constrainedLeft + 'px';
widgetButton.style.top = constrainedTop + 'px';
widgetButton.style.right = 'auto';
widgetButton.style.bottom = 'auto';
// 保持 scale 效果
widgetButton.style.transform = 'scale(1.1)';
}
// 停止拖拽
function stopDrag() {
if (!isDragging) return;
isDragging = false;
// 取消待执行的动画帧
if (dragAnimationFrame) {
cancelAnimationFrame(dragAnimationFrame);
dragAnimationFrame = null;
}
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
widgetButton.classList.remove('dragging');
widgetButton.style.zIndex = '9999';
// 恢复过渡效果
widgetButton.style.transition = '';
widgetButton.style.willChange = '';
// 移除 transform让 CSS hover 效果可以正常工作
widgetButton.style.transform = '';
// 根据按钮类型和当前位置进行最终定位
requestAnimationFrame(() => {
const buttonRect = widgetButton.getBoundingClientRect();
const currentLeft = buttonRect.left;
const currentTop = buttonRect.top;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const buttonWidth = buttonSize.width;
const buttonHeight = buttonSize.height;
// 两种模式使用相同的停止拖拽逻辑:只能左右侧边缘吸附
// 根据按钮实际位置判断左右,保持当前位置
const screenCenterX = windowWidth / 2;
const buttonCenterX = currentLeft + buttonWidth / 2;
const isLeftSide = buttonCenterX < screenCenterX;
const sideDistance = 16; // 距离边缘的距离
// 垂直位置:保持在当前位置,限制在屏幕范围内,距离顶部和底部最小距离为 24px
const minTop = 24;
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
const finalTop = Math.max(minTop, Math.min(currentTop, maxTop));
let finalLeft;
// 水平位置距离左右边16px
if (isLeftSide) {
finalLeft = sideDistance;
widgetButton.style.left = sideDistance + 'px';
widgetButton.style.right = 'auto';
} else {
finalLeft = windowWidth - sideDistance - buttonWidth;
widgetButton.style.right = sideDistance + 'px';
widgetButton.style.left = 'auto';
}
widgetButton.style.top = finalTop + 'px';
widgetButton.style.bottom = 'auto';
// 更新 border-radius现在都是24px圆角
widgetButton.style.borderRadius = '24px';
// 更新初始位置,为下次拖拽做准备
if (finalLeft !== undefined && finalTop !== undefined) {
initialPosition.left = finalLeft;
initialPosition.top = finalTop;
} else {
// 如果未定义,使用当前实际位置
initialPosition.left = buttonRect.left;
initialPosition.top = buttonRect.top;
}
});
}
// 设置按钮状态
function setButtonState(state) {
if (!widgetButton) return;
widgetButton.classList.remove('success', 'error', 'loading');
if (state === 'success') {
widgetButton.classList.add('success');
} else if (state === 'error') {
widgetButton.classList.add('error');
} else if (state === 'loading') {
widgetButton.classList.add('loading');
}
}
// 更新主题模式
function updateThemeMode(theme_mode) {
if (theme_mode === 'light' || theme_mode === 'dark') {
applyTheme(theme_mode);
}
}
// 全局函数
window.hideWidgetModal = hideModal;
window.setWidgetButtonState = setButtonState;
window.updateWidgetTheme = updateThemeMode;
// 点击模态框背景关闭
document.addEventListener('click', function (e) {
if (e.target === widgetModal) {
hideModal();
}
});
// 窗口大小改变时重新定位
window.addEventListener('resize', function () {
if (widgetModal && widgetModal.style.display === 'flex') {
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
if (!modalContent) return;
// 移动端强制居中显示
if (isMobile()) {
modalContent.style.position = 'relative';
modalContent.style.top = 'auto';
modalContent.style.left = 'auto';
modalContent.style.right = 'auto';
modalContent.style.bottom = 'auto';
modalContent.style.margin = 'auto';
modalContent.style.width = 'calc(100% - 32px)';
modalContent.style.height = 'auto';
return;
}
const modalPosition = widgetInfo?.modal_position || defaultModalPosition;
if (modalPosition === 'fixed') {
// 固定居中模式不需要重新定位
return;
}
// 重新计算模态框位置(使用智能定位)
positionModalFollow(modalContent);
}
});
// 初始化
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fetchWidgetInfo);
} else {
fetchWidgetInfo();
}
}
// 页面卸载时清理
window.addEventListener('beforeunload', function () {
if (widgetButton) {
widgetButton.remove();
}
if (widgetModal) {
widgetModal.remove();
}
if (customTriggerElement && customTriggerHandler) {
customTriggerElement.removeEventListener('click', customTriggerHandler);
customTriggerElement.removeAttribute('data-widget-trigger-attached');
}
});
// 启动
init();
})();