Browse Source

支持图标拖拽固定

dev-mcp
wanghanlin 3 weeks ago
parent
commit
4846852d26
  1. 284
      client/dist/chatbot-sdk.js
  2. 2
      client/dist/chatbot-sdk.js.map
  3. 2
      client/dist/chatbot-sdk.min.js
  4. 2
      client/dist/chatbot-sdk.min.js.map
  5. 158
      client/src/dom.ts
  6. 71
      client/src/index.ts
  7. 26
      client/src/styles.ts
  8. 251
      src/main/resources/static/sdk/chatbot-sdk.js
  9. 2
      src/main/resources/static/sdk/chatbot-sdk.js.map
  10. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js
  11. 2
      src/main/resources/static/sdk/chatbot-sdk.min.js.map

284
client/dist/chatbot-sdk.js

@ -788,15 +788,18 @@ var ChatbotSDK = (function () {
}
}
/**
* 获取会话列表
* 获取会话列表SDK 安全端点
* 使用 /ai/sdk/conversation/list强制按 accountId + roleId 过滤
*/
async function fetchConversationList(page = 1, size = 20, accountId, roleId) {
let path = `/conversation/list?page=${page}&size=${size}`;
const params = new URLSearchParams();
params.set('page', String(page));
params.set('size', String(size));
if (accountId)
path += `&accountId=${encodeURIComponent(accountId)}`;
params.set('accountId', accountId);
if (roleId)
path += `&roleId=${encodeURIComponent(roleId)}`;
const url = buildUrl(path);
params.set('roleId', roleId);
const url = buildUrl(`/ai/sdk/conversation/list?${params.toString()}`);
try {
const response = await safeFetch(url);
if (!response.ok)
@ -814,10 +817,16 @@ var ChatbotSDK = (function () {
}
}
/**
* 获取会话消息
* 获取会话消息SDK 安全端点
* 使用 /ai/sdk/conversation/{id}/messages含账户归属校验
*/
async function fetchConversationMessages(conversationId) {
const url = buildUrl(`/conversation/${conversationId}/messages`);
const params = new URLSearchParams();
if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId)
params.set('accountId', currentConfig.userId);
if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId)
params.set('roleId', currentConfig.integrateId);
const url = buildUrl(`/ai/sdk/conversation/${conversationId}/messages?${params.toString()}`);
try {
const response = await safeFetch(url);
if (!response.ok)
@ -834,10 +843,16 @@ var ChatbotSDK = (function () {
}
}
/**
* 删除会话
* 删除会话SDK 安全端点
* 使用 DELETE /ai/sdk/conversation/{id}含账户归属校验
*/
async function deleteConversation(conversationId) {
const url = buildUrl(`/conversation/${conversationId}`);
const params = new URLSearchParams();
if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId)
params.set('accountId', currentConfig.userId);
if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId)
params.set('roleId', currentConfig.integrateId);
const url = buildUrl(`/ai/sdk/conversation/${conversationId}?${params.toString()}`);
try {
const response = await safeFetch(url, { method: 'DELETE' });
if (!response.ok)
@ -852,10 +867,16 @@ var ChatbotSDK = (function () {
}
}
/**
* 导出会话 URL
* 导出会话 URLSDK 安全端点
* 使用 /ai/sdk/conversation/{id}/export含账户归属参数
*/
function getConversationExportUrl(conversationId) {
return buildUrl(`/conversation/${conversationId}/export`);
const params = new URLSearchParams();
if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId)
params.set('accountId', currentConfig.userId);
if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId)
params.set('roleId', currentConfig.integrateId);
return buildUrl(`/ai/sdk/conversation/${conversationId}/export?${params.toString()}`);
}
// ==================== chatId 自动初始化 ====================
/**
@ -863,7 +884,7 @@ var ChatbotSDK = (function () {
*
* 逻辑
* 1. 先查 localStorage 缓存的 chatId同一 integrateId + userId 可能复用
* 2. /conversation/list?accountId=X&roleId=Y 看是否有匹配的会话
* 2. /ai/sdk/conversation/list?accountId=X&roleId=Y 看是否有匹配的会话
* 3. 有会话 使用最新会话的 conversationId 作为 chatId
* 4. 无会话 自动生成 chatId格式sdk_timestamp_random
*/
@ -2126,6 +2147,25 @@ var ChatbotSDK = (function () {
.csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); }
.csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); }
/* Launcher 拖拽态:微放大 + 阴影加深 */
.csk-launcher--dragging {
cursor: grabbing !important;
transform: scale(1.1) !important;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.14),
0 12px 32px rgba(0, 0, 0, 0.18),
0 14px 40px rgba(var(--csk-primary-rgb), 0.36),
0 0 56px rgba(var(--csk-primary-rgb), 0.18) !important;
}
.csk-launcher--dragging::before,
.csk-launcher--dragging::after { animation-play-state: paused !important; }
/* 拖拽中隐藏未读徽章 */
.csk-launcher--dragging .csk-launcher__badge { display: none !important; }
/* 吸附过渡动画(释放时的缓动) */
.csk-launcher--snap {
transition: left 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out !important;
}
/* ========== Launcher 未读徽章 ========== */
.csk-launcher__badge {
position: absolute;
@ -2308,9 +2348,10 @@ var ChatbotSDK = (function () {
border-radius: 0;
}
.csk-window--hidden { transform: translateY(100%); }
.csk-launcher { bottom: 20px; }
.csk-launcher--right { right: 20px; }
.csk-launcher--left { left: 20px; }
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */
.csk-launcher:not([style*="bottom"]) { bottom: 20px; }
.csk-launcher--right:not([style*="bottom"]) { right: 20px; }
.csk-launcher--left:not([style*="bottom"]) { left: 20px; }
}
`;
}
@ -2723,6 +2764,150 @@ var ChatbotSDK = (function () {
document.removeEventListener('mouseup', onMouseUp);
};
}
/** Launcher 拖拽:垂直方向最小 bottom 值(px),避免贴底被系统导航栏遮挡 */
const LAUNCHER_MIN_BOTTOM = 16;
/** Launcher 拖拽:计算当前视口下最大 bottom 值 */
function launcherMaxBottom() {
return window.innerHeight - 76; // 按钮高度 60 + 16 间距
}
/**
* 启用 Launcher 按钮的拖拽功能边缘吸附式
*
* 设计要点
* - 仅允许垂直方向自由移动水平方向释放时自动吸附到最近的左/右边缘
* - 移动距离 < 5px 视为点击不干扰原有的 open/close 逻辑
* - 拖拽中按钮微放大 + 隐藏未读徽章释放后带缓动吸附动画
* - 支持鼠标 + 触摸事件触摸拖拽时阻止页面滚动
*/
function enableLauncherDrag(launcherEl, config, onDragEnd) {
let dragging = false;
let startX = 0;
let startY = 0;
let hasMoved = false;
let startBottom = 0;
let startRight = 0;
let startLeft = 0;
// 标记是否正在吸附过渡中,避免过渡期间的 mouseup 再次触发
let isSnapping = false;
/** 获取当前 bottom 值(纯数值) */
function getCurrentBottom() {
const val = parseFloat(getComputedStyle(launcherEl).bottom);
return isNaN(val) ? 24 : val;
}
function onPointerDown(clientX, clientY) {
if (isSnapping)
return;
dragging = true;
hasMoved = false;
startX = clientX;
startY = clientY;
startBottom = getCurrentBottom();
// 记录水平起始位置(用于判断当前在哪一侧)
const style = launcherEl.style;
const cs = getComputedStyle(launcherEl);
startRight = style.right ? parseFloat(style.right) : parseFloat(cs.right);
startLeft = style.left ? parseFloat(style.left) : parseFloat(cs.left);
}
function onPointerMove(clientX, clientY, e) {
if (!dragging)
return;
const dx = clientX - startX;
const dy = clientY - startY;
if (!hasMoved) {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5)
return;
hasMoved = true;
// 进入拖拽态
launcherEl.classList.add('csk-launcher--dragging');
launcherEl.classList.remove('csk-launcher--right', 'csk-launcher--left');
launcherEl.style.transition = 'none';
// 同步当前水平位置(从吸附态的 right/left 转换为固定像素)
if (!isNaN(startRight) && startRight >= 0 && isNaN(startLeft)) {
launcherEl.style.right = `${startRight}px`;
launcherEl.style.left = 'auto';
}
else if (!isNaN(startLeft) && startLeft >= 0) {
launcherEl.style.left = `${startLeft}px`;
launcherEl.style.right = 'auto';
}
}
if (hasMoved) {
e.preventDefault(); // 阻止触摸滚动
const newBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(startBottom - dy, launcherMaxBottom()));
launcherEl.style.bottom = `${newBottom}px`;
}
}
function onPointerUp() {
if (!dragging)
return;
dragging = false;
if (!hasMoved)
return; // 移动距离不足 5px,视为点击,不处理
// 隐藏拖拽态(保留当前 bottom)
const currentBottom = parseFloat(launcherEl.style.bottom) || getCurrentBottom();
const clampedBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(currentBottom, launcherMaxBottom()));
// 判断吸附到哪一侧(取离哪边更近)
const rect = launcherEl.getBoundingClientRect();
const center = rect.left + rect.width / 2;
const snapSide = center <= window.innerWidth / 2 ? 'left' : 'right';
// 切换到吸附过渡动画
launcherEl.classList.remove('csk-launcher--dragging');
launcherEl.classList.add(`csk-launcher--${snapSide}`, 'csk-launcher--snap');
launcherEl.style.left = '';
launcherEl.style.right = '';
launcherEl.style.bottom = `${clampedBottom}px`;
// 过渡结束后清理(保留 bottom,否则 CSS 默认 24px 会覆盖拖拽位置)
isSnapping = true;
const onTransitionEnd = () => {
launcherEl.removeEventListener('transitionend', onTransitionEnd);
launcherEl.classList.remove('csk-launcher--snap');
launcherEl.style.transition = '';
// 注意:不清除 style.bottom,保留用户拖拽的垂直位置
isSnapping = false;
};
launcherEl.addEventListener('transitionend', onTransitionEnd);
// 兜底:350ms 后强制清理(transitionend 可能不触发)
setTimeout(() => {
if (isSnapping)
onTransitionEnd();
}, 350);
// 恢复未读徽章
const badge = launcherEl.querySelector('.csk-launcher__badge');
if (badge)
badge.style.display = '';
if (onDragEnd) {
onDragEnd({ side: snapSide, bottom: clampedBottom });
}
}
// --- 鼠标事件 ---
function onMouseDown(e) { onPointerDown(e.clientX, e.clientY); }
function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); }
function onMouseUp() { onPointerUp(); }
// --- 触摸事件 ---
function onTouchStart(e) {
if (e.touches.length === 1)
onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
}
function onTouchMove(e) {
if (e.touches.length === 1)
onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e);
}
function onTouchEnd() { onPointerUp(); }
launcherEl.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
launcherEl.addEventListener('touchstart', onTouchStart, { passive: true });
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
return () => {
launcherEl.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
launcherEl.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
}
// ==================== 消息渲染 ====================
/** 渲染用户消息气泡 */
function renderUserBubble(container, text, timestamp) {
@ -4171,6 +4356,9 @@ var ChatbotSDK = (function () {
let showLoadingFn = null;
let hideLoadingFn = null;
let dragCleanup = null;
let launcherDragCleanup = null;
/** Launcher 位置记忆状态(拖拽后更新) */
let launcherPos = null;
/** 提示气泡元素 */
let teaserEl = null;
/** 提示气泡定时器 */
@ -4206,6 +4394,15 @@ var ChatbotSDK = (function () {
document.body.appendChild(launcherEl);
// 获取未读徽章引用(由 createLauncher 创建)
badgeEl = launcherEl.querySelector('.csk-launcher__badge');
// 6.1 恢复 Launcher 位置(必须在挂载到 DOM 之后、拖拽绑定之前)
restoreLauncherPosition(config.integrateId, launcherEl);
// 6.2 启用 Launcher 拖拽(边缘吸附式,支持触摸 + 鼠标)
launcherDragCleanup = enableLauncherDrag(launcherEl, config, (pos) => {
launcherPos = pos;
saveLauncherPosition(config.integrateId, pos);
// Launcher 位置变更后,同步聊天弹窗对齐侧
syncWindowAlignment(pos.side);
});
// 7. 创建聊天弹窗
const dom = createChatWindow(config);
windowEl = dom.window;
@ -4310,15 +4507,21 @@ var ChatbotSDK = (function () {
function positionTeaser() {
if (!teaserEl || !launcherEl)
return;
// launcher 被拖拽过则用实际位置,否则用 DOM 实时计算
const rect = launcherEl.getBoundingClientRect();
const isLeft = (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom';
const isLeft = launcherPos ? launcherPos.side === 'left' : (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom';
if (isLeft) {
teaserEl.style.left = `${rect.left}px`;
teaserEl.style.right = 'auto';
}
else {
teaserEl.style.right = `${window.innerWidth - rect.right}px`;
teaserEl.style.left = 'auto';
}
teaserEl.style.bottom = `${window.innerHeight - rect.top + 8}px`;
// 同步 teaser 类名(箭头方向)
teaserEl.classList.toggle('csk-teaser--left', !!isLeft);
teaserEl.classList.toggle('csk-teaser--right', !isLeft);
}
function showTeaser() {
if (!teaserEl)
@ -4439,6 +4642,11 @@ var ChatbotSDK = (function () {
dragCleanup();
dragCleanup = null;
}
if (launcherDragCleanup) {
launcherDragCleanup();
launcherDragCleanup = null;
}
launcherPos = null;
removeStyles();
document.removeEventListener('keydown', onKeyDown);
window.removeEventListener('resize', onResize);
@ -4525,6 +4733,50 @@ var ChatbotSDK = (function () {
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
// ==================== Launcher 位置记忆 ====================
/** localStorage key:Launcher 位置 */
function launcherPosKey(integrateId) {
return `csk_launcher_pos_${integrateId}`;
}
/** 保存 Launcher 拖拽位置到 localStorage */
function saveLauncherPosition(integrateId, pos) {
try {
localStorage.setItem(launcherPosKey(integrateId), JSON.stringify(pos));
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
/** 恢复 Launcher 位置(初始化时调用) */
function restoreLauncherPosition(integrateId, el) {
try {
const raw = localStorage.getItem(launcherPosKey(integrateId));
if (!raw)
return;
const pos = JSON.parse(raw);
if ((pos.side !== 'left' && pos.side !== 'right') || typeof pos.bottom !== 'number')
return;
const maxBottom = window.innerHeight - 76;
const clampedBottom = Math.max(16, Math.min(pos.bottom, maxBottom));
// 覆盖默认对齐方向
el.classList.remove('csk-launcher--right', 'csk-launcher--left');
el.classList.add(`csk-launcher--${pos.side}`);
el.style.bottom = `${clampedBottom}px`;
// 保存到模块状态,供后续 positionTeaser / 弹窗对齐使用
launcherPos = { side: pos.side, bottom: clampedBottom };
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
/** 同步聊天弹窗对齐侧(Launcher 拖拽到左/右侧时跟随) */
function syncWindowAlignment(side) {
if (!windowEl)
return;
windowEl.classList.remove('csk-window--right', 'csk-window--left');
windowEl.classList.add(`csk-window--${side}`);
// 同步 teaser 箭头方向
if (teaserEl) {
teaserEl.classList.remove('csk-teaser--right', 'csk-teaser--left');
teaserEl.classList.add(`csk-teaser--${side}`);
}
}
// ==================== 提示音 + 桌面通知 ====================
/** 播放短促提示音(Web Audio API 合成,无需音频文件) */
function playNotificationSound() {

2
client/dist/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
client/dist/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

158
client/src/dom.ts

@ -421,6 +421,164 @@ export function enableDrag(headerEl: HTMLElement, windowEl: HTMLElement, onDragE
};
}
/** Launcher 拖拽:垂直方向最小 bottom 值(px),避免贴底被系统导航栏遮挡 */
const LAUNCHER_MIN_BOTTOM = 16;
/** Launcher 拖拽:计算当前视口下最大 bottom 值 */
function launcherMaxBottom(): number {
return window.innerHeight - 76; // 按钮高度 60 + 16 间距
}
/**
* Launcher
*
*
* - /
* - < 5px open/close
* - +
* - +
*/
export function enableLauncherDrag(
launcherEl: HTMLElement,
config: Pick<ResolvedConfig, 'position'>,
onDragEnd?: (pos: { side: 'left' | 'right'; bottom: number }) => void,
): () => void {
let dragging = false;
let startX = 0;
let startY = 0;
let hasMoved = false;
let startBottom = 0;
let startRight = 0;
let startLeft = 0;
// 标记是否正在吸附过渡中,避免过渡期间的 mouseup 再次触发
let isSnapping = false;
/** 获取当前 bottom 值(纯数值) */
function getCurrentBottom(): number {
const val = parseFloat(getComputedStyle(launcherEl).bottom);
return isNaN(val) ? 24 : val;
}
function onPointerDown(clientX: number, clientY: number): void {
if (isSnapping) return;
dragging = true;
hasMoved = false;
startX = clientX;
startY = clientY;
startBottom = getCurrentBottom();
// 记录水平起始位置(用于判断当前在哪一侧)
const style = launcherEl.style;
const cs = getComputedStyle(launcherEl);
startRight = style.right ? parseFloat(style.right) : parseFloat(cs.right);
startLeft = style.left ? parseFloat(style.left) : parseFloat(cs.left);
}
function onPointerMove(clientX: number, clientY: number, e: Event): void {
if (!dragging) return;
const dx = clientX - startX;
const dy = clientY - startY;
if (!hasMoved) {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) return;
hasMoved = true;
// 进入拖拽态
launcherEl.classList.add('csk-launcher--dragging');
launcherEl.classList.remove('csk-launcher--right', 'csk-launcher--left');
launcherEl.style.transition = 'none';
// 同步当前水平位置(从吸附态的 right/left 转换为固定像素)
if (!isNaN(startRight) && startRight >= 0 && isNaN(startLeft)) {
launcherEl.style.right = `${startRight}px`;
launcherEl.style.left = 'auto';
} else if (!isNaN(startLeft) && startLeft >= 0) {
launcherEl.style.left = `${startLeft}px`;
launcherEl.style.right = 'auto';
}
}
if (hasMoved) {
e.preventDefault(); // 阻止触摸滚动
const newBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(startBottom - dy, launcherMaxBottom()));
launcherEl.style.bottom = `${newBottom}px`;
}
}
function onPointerUp(): void {
if (!dragging) return;
dragging = false;
if (!hasMoved) return; // 移动距离不足 5px,视为点击,不处理
// 隐藏拖拽态(保留当前 bottom)
const currentBottom = parseFloat(launcherEl.style.bottom) || getCurrentBottom();
const clampedBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(currentBottom, launcherMaxBottom()));
// 判断吸附到哪一侧(取离哪边更近)
const rect = launcherEl.getBoundingClientRect();
const center = rect.left + rect.width / 2;
const snapSide: 'left' | 'right' = center <= window.innerWidth / 2 ? 'left' : 'right';
// 切换到吸附过渡动画
launcherEl.classList.remove('csk-launcher--dragging');
launcherEl.classList.add(`csk-launcher--${snapSide}`, 'csk-launcher--snap');
launcherEl.style.left = '';
launcherEl.style.right = '';
launcherEl.style.bottom = `${clampedBottom}px`;
// 过渡结束后清理(保留 bottom,否则 CSS 默认 24px 会覆盖拖拽位置)
isSnapping = true;
const onTransitionEnd = (): void => {
launcherEl.removeEventListener('transitionend', onTransitionEnd);
launcherEl.classList.remove('csk-launcher--snap');
launcherEl.style.transition = '';
// 注意:不清除 style.bottom,保留用户拖拽的垂直位置
isSnapping = false;
};
launcherEl.addEventListener('transitionend', onTransitionEnd);
// 兜底:350ms 后强制清理(transitionend 可能不触发)
setTimeout(() => {
if (isSnapping) onTransitionEnd();
}, 350);
// 恢复未读徽章
const badge = launcherEl.querySelector('.csk-launcher__badge');
if (badge) (badge as HTMLElement).style.display = '';
if (onDragEnd) {
onDragEnd({ side: snapSide, bottom: clampedBottom });
}
}
// --- 鼠标事件 ---
function onMouseDown(e: MouseEvent): void { onPointerDown(e.clientX, e.clientY); }
function onMouseMove(e: MouseEvent): void { onPointerMove(e.clientX, e.clientY, e); }
function onMouseUp(): void { onPointerUp(); }
// --- 触摸事件 ---
function onTouchStart(e: TouchEvent): void {
if (e.touches.length === 1) onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
}
function onTouchMove(e: TouchEvent): void {
if (e.touches.length === 1) onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e);
}
function onTouchEnd(): void { onPointerUp(); }
launcherEl.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
launcherEl.addEventListener('touchstart', onTouchStart, { passive: true });
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
return () => {
launcherEl.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
launcherEl.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
}
// ==================== 消息渲染 ====================
/** 渲染用户消息气泡 */

71
client/src/index.ts

@ -12,7 +12,7 @@ import { parseConfig } from './config';
import { setDebug, logger, setErrorCallback } from './logger';
import { setApiConfig } from './api';
import { injectStyles, removeStyles } from './styles';
import { createLauncher, createChatWindow, enableDrag } from './dom';
import { createLauncher, createChatWindow, enableDrag, enableLauncherDrag } from './dom';
import { initChat, initChatHistory, getMessages, setCategory, loadHistoryConversations, sendQuickReply, retryFromMessage, handleFeedback } from './chat';
import { clearMessages } from './storage';
import { setLocale } from './i18n';
@ -45,6 +45,10 @@ let historyPanel: HTMLElement | null = null;
let showLoadingFn: (() => HTMLElement) | null = null;
let hideLoadingFn: (() => void) | null = null;
let dragCleanup: (() => void) | null = null;
let launcherDragCleanup: (() => void) | null = null;
/** Launcher 位置记忆状态(拖拽后更新) */
let launcherPos: { side: 'left' | 'right'; bottom: number } | null = null;
/** 提示气泡元素 */
let teaserEl: HTMLElement | null = null;
@ -92,6 +96,17 @@ function init(rawConfig: SDKConfig): void {
// 获取未读徽章引用(由 createLauncher 创建)
badgeEl = launcherEl.querySelector('.csk-launcher__badge');
// 6.1 恢复 Launcher 位置(必须在挂载到 DOM 之后、拖拽绑定之前)
restoreLauncherPosition(config.integrateId, launcherEl);
// 6.2 启用 Launcher 拖拽(边缘吸附式,支持触摸 + 鼠标)
launcherDragCleanup = enableLauncherDrag(launcherEl, config, (pos) => {
launcherPos = pos;
saveLauncherPosition(config!.integrateId, pos);
// Launcher 位置变更后,同步聊天弹窗对齐侧
syncWindowAlignment(pos.side);
});
// 7. 创建聊天弹窗
const dom = createChatWindow(config);
windowEl = dom.window;
@ -206,14 +221,20 @@ function init(rawConfig: SDKConfig): void {
/** 根据 launcher 位置定位 teaser 气泡 */
function positionTeaser(): void {
if (!teaserEl || !launcherEl) return;
// launcher 被拖拽过则用实际位置,否则用 DOM 实时计算
const rect = launcherEl.getBoundingClientRect();
const isLeft = config?.position === 'left-bottom';
const isLeft = launcherPos ? launcherPos.side === 'left' : config?.position === 'left-bottom';
if (isLeft) {
teaserEl.style.left = `${rect.left}px`;
teaserEl.style.right = 'auto';
} else {
teaserEl.style.right = `${window.innerWidth - rect.right}px`;
teaserEl.style.left = 'auto';
}
teaserEl.style.bottom = `${window.innerHeight - rect.top + 8}px`;
// 同步 teaser 类名(箭头方向)
teaserEl.classList.toggle('csk-teaser--left', !!isLeft);
teaserEl.classList.toggle('csk-teaser--right', !isLeft);
}
function showTeaser(): void {
@ -326,6 +347,8 @@ function destroy(): void {
if (launcherEl && launcherEl.parentNode) { launcherEl.parentNode.removeChild(launcherEl); launcherEl = null; }
if (windowEl && windowEl.parentNode) { windowEl.parentNode.removeChild(windowEl); windowEl = null; }
if (dragCleanup) { dragCleanup(); dragCleanup = null; }
if (launcherDragCleanup) { launcherDragCleanup(); launcherDragCleanup = null; }
launcherPos = null;
removeStyles();
document.removeEventListener('keydown', onKeyDown);
@ -410,6 +433,50 @@ function restoreWindowPosition(integrateId: string, winEl: HTMLElement): void {
} catch { /* 忽略 */ }
}
// ==================== Launcher 位置记忆 ====================
/** localStorage key:Launcher 位置 */
function launcherPosKey(integrateId: string): string {
return `csk_launcher_pos_${integrateId}`;
}
/** 保存 Launcher 拖拽位置到 localStorage */
function saveLauncherPosition(integrateId: string, pos: { side: 'left' | 'right'; bottom: number }): void {
try {
localStorage.setItem(launcherPosKey(integrateId), JSON.stringify(pos));
} catch { /* 忽略 */ }
}
/** 恢复 Launcher 位置(初始化时调用) */
function restoreLauncherPosition(integrateId: string, el: HTMLElement): void {
try {
const raw = localStorage.getItem(launcherPosKey(integrateId));
if (!raw) return;
const pos = JSON.parse(raw) as { side: 'left' | 'right'; bottom: number };
if ((pos.side !== 'left' && pos.side !== 'right') || typeof pos.bottom !== 'number') return;
const maxBottom = window.innerHeight - 76;
const clampedBottom = Math.max(16, Math.min(pos.bottom, maxBottom));
// 覆盖默认对齐方向
el.classList.remove('csk-launcher--right', 'csk-launcher--left');
el.classList.add(`csk-launcher--${pos.side}`);
el.style.bottom = `${clampedBottom}px`;
// 保存到模块状态,供后续 positionTeaser / 弹窗对齐使用
launcherPos = { side: pos.side, bottom: clampedBottom };
} catch { /* 忽略 */ }
}
/** 同步聊天弹窗对齐侧(Launcher 拖拽到左/右侧时跟随) */
function syncWindowAlignment(side: 'left' | 'right'): void {
if (!windowEl) return;
windowEl.classList.remove('csk-window--right', 'csk-window--left');
windowEl.classList.add(`csk-window--${side}`);
// 同步 teaser 箭头方向
if (teaserEl) {
teaserEl.classList.remove('csk-teaser--right', 'csk-teaser--left');
teaserEl.classList.add(`csk-teaser--${side}`);
}
}
// ==================== 提示音 + 桌面通知 ====================
/** 播放短促提示音(Web Audio API 合成,无需音频文件) */

26
client/src/styles.ts

@ -1201,6 +1201,25 @@ function getStyles(config: ResolvedConfig): string {
.csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); }
.csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); }
/* Launcher 拖拽态:微放大 + 阴影加深 */
.csk-launcher--dragging {
cursor: grabbing !important;
transform: scale(1.1) !important;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.14),
0 12px 32px rgba(0, 0, 0, 0.18),
0 14px 40px rgba(var(--csk-primary-rgb), 0.36),
0 0 56px rgba(var(--csk-primary-rgb), 0.18) !important;
}
.csk-launcher--dragging::before,
.csk-launcher--dragging::after { animation-play-state: paused !important; }
/* 拖拽中隐藏未读徽章 */
.csk-launcher--dragging .csk-launcher__badge { display: none !important; }
/* 吸附过渡动画(释放时的缓动) */
.csk-launcher--snap {
transition: left 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out !important;
}
/* ========== Launcher 未读徽章 ========== */
.csk-launcher__badge {
position: absolute;
@ -1383,9 +1402,10 @@ function getStyles(config: ResolvedConfig): string {
border-radius: 0;
}
.csk-window--hidden { transform: translateY(100%); }
.csk-launcher { bottom: 20px; }
.csk-launcher--right { right: 20px; }
.csk-launcher--left { left: 20px; }
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */
.csk-launcher:not([style*="bottom"]) { bottom: 20px; }
.csk-launcher--right:not([style*="bottom"]) { right: 20px; }
.csk-launcher--left:not([style*="bottom"]) { left: 20px; }
}
`;
}

251
src/main/resources/static/sdk/chatbot-sdk.js

@ -788,7 +788,8 @@ var ChatbotSDK = (function () {
}
}
/**
* 获取会话列表SDK 安全端点强制按 accountId + roleId 过滤
* 获取会话列表SDK 安全端点
* 使用 /ai/sdk/conversation/list强制按 accountId + roleId 过滤
*/
async function fetchConversationList(page = 1, size = 20, accountId, roleId) {
const params = new URLSearchParams();
@ -816,7 +817,8 @@ var ChatbotSDK = (function () {
}
}
/**
* 获取会话消息SDK 安全端点含账户归属校验
* 获取会话消息SDK 安全端点
* 使用 /ai/sdk/conversation/{id}/messages含账户归属校验
*/
async function fetchConversationMessages(conversationId) {
const params = new URLSearchParams();
@ -841,7 +843,8 @@ var ChatbotSDK = (function () {
}
}
/**
* 删除会话SDK 安全端点含账户归属校验
* 删除会话SDK 安全端点
* 使用 DELETE /ai/sdk/conversation/{id}含账户归属校验
*/
async function deleteConversation(conversationId) {
const params = new URLSearchParams();
@ -864,7 +867,8 @@ var ChatbotSDK = (function () {
}
}
/**
* 导出会话 URLSDK 安全端点含账户归属参数
* 导出会话 URLSDK 安全端点
* 使用 /ai/sdk/conversation/{id}/export含账户归属参数
*/
function getConversationExportUrl(conversationId) {
const params = new URLSearchParams();
@ -2143,6 +2147,25 @@ var ChatbotSDK = (function () {
.csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); }
.csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); }
/* Launcher 拖拽态:微放大 + 阴影加深 */
.csk-launcher--dragging {
cursor: grabbing !important;
transform: scale(1.1) !important;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.14),
0 12px 32px rgba(0, 0, 0, 0.18),
0 14px 40px rgba(var(--csk-primary-rgb), 0.36),
0 0 56px rgba(var(--csk-primary-rgb), 0.18) !important;
}
.csk-launcher--dragging::before,
.csk-launcher--dragging::after { animation-play-state: paused !important; }
/* 拖拽中隐藏未读徽章 */
.csk-launcher--dragging .csk-launcher__badge { display: none !important; }
/* 吸附过渡动画(释放时的缓动) */
.csk-launcher--snap {
transition: left 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out !important;
}
/* ========== Launcher 未读徽章 ========== */
.csk-launcher__badge {
position: absolute;
@ -2325,9 +2348,10 @@ var ChatbotSDK = (function () {
border-radius: 0;
}
.csk-window--hidden { transform: translateY(100%); }
.csk-launcher { bottom: 20px; }
.csk-launcher--right { right: 20px; }
.csk-launcher--left { left: 20px; }
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */
.csk-launcher:not([style*="bottom"]) { bottom: 20px; }
.csk-launcher--right:not([style*="bottom"]) { right: 20px; }
.csk-launcher--left:not([style*="bottom"]) { left: 20px; }
}
`;
}
@ -2740,6 +2764,150 @@ var ChatbotSDK = (function () {
document.removeEventListener('mouseup', onMouseUp);
};
}
/** Launcher 拖拽:垂直方向最小 bottom 值(px),避免贴底被系统导航栏遮挡 */
const LAUNCHER_MIN_BOTTOM = 16;
/** Launcher 拖拽:计算当前视口下最大 bottom 值 */
function launcherMaxBottom() {
return window.innerHeight - 76; // 按钮高度 60 + 16 间距
}
/**
* 启用 Launcher 按钮的拖拽功能边缘吸附式
*
* 设计要点
* - 仅允许垂直方向自由移动水平方向释放时自动吸附到最近的左/右边缘
* - 移动距离 < 5px 视为点击不干扰原有的 open/close 逻辑
* - 拖拽中按钮微放大 + 隐藏未读徽章释放后带缓动吸附动画
* - 支持鼠标 + 触摸事件触摸拖拽时阻止页面滚动
*/
function enableLauncherDrag(launcherEl, config, onDragEnd) {
let dragging = false;
let startX = 0;
let startY = 0;
let hasMoved = false;
let startBottom = 0;
let startRight = 0;
let startLeft = 0;
// 标记是否正在吸附过渡中,避免过渡期间的 mouseup 再次触发
let isSnapping = false;
/** 获取当前 bottom 值(纯数值) */
function getCurrentBottom() {
const val = parseFloat(getComputedStyle(launcherEl).bottom);
return isNaN(val) ? 24 : val;
}
function onPointerDown(clientX, clientY) {
if (isSnapping)
return;
dragging = true;
hasMoved = false;
startX = clientX;
startY = clientY;
startBottom = getCurrentBottom();
// 记录水平起始位置(用于判断当前在哪一侧)
const style = launcherEl.style;
const cs = getComputedStyle(launcherEl);
startRight = style.right ? parseFloat(style.right) : parseFloat(cs.right);
startLeft = style.left ? parseFloat(style.left) : parseFloat(cs.left);
}
function onPointerMove(clientX, clientY, e) {
if (!dragging)
return;
const dx = clientX - startX;
const dy = clientY - startY;
if (!hasMoved) {
if (Math.abs(dx) < 5 && Math.abs(dy) < 5)
return;
hasMoved = true;
// 进入拖拽态
launcherEl.classList.add('csk-launcher--dragging');
launcherEl.classList.remove('csk-launcher--right', 'csk-launcher--left');
launcherEl.style.transition = 'none';
// 同步当前水平位置(从吸附态的 right/left 转换为固定像素)
if (!isNaN(startRight) && startRight >= 0 && isNaN(startLeft)) {
launcherEl.style.right = `${startRight}px`;
launcherEl.style.left = 'auto';
}
else if (!isNaN(startLeft) && startLeft >= 0) {
launcherEl.style.left = `${startLeft}px`;
launcherEl.style.right = 'auto';
}
}
if (hasMoved) {
e.preventDefault(); // 阻止触摸滚动
const newBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(startBottom - dy, launcherMaxBottom()));
launcherEl.style.bottom = `${newBottom}px`;
}
}
function onPointerUp() {
if (!dragging)
return;
dragging = false;
if (!hasMoved)
return; // 移动距离不足 5px,视为点击,不处理
// 隐藏拖拽态(保留当前 bottom)
const currentBottom = parseFloat(launcherEl.style.bottom) || getCurrentBottom();
const clampedBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(currentBottom, launcherMaxBottom()));
// 判断吸附到哪一侧(取离哪边更近)
const rect = launcherEl.getBoundingClientRect();
const center = rect.left + rect.width / 2;
const snapSide = center <= window.innerWidth / 2 ? 'left' : 'right';
// 切换到吸附过渡动画
launcherEl.classList.remove('csk-launcher--dragging');
launcherEl.classList.add(`csk-launcher--${snapSide}`, 'csk-launcher--snap');
launcherEl.style.left = '';
launcherEl.style.right = '';
launcherEl.style.bottom = `${clampedBottom}px`;
// 过渡结束后清理(保留 bottom,否则 CSS 默认 24px 会覆盖拖拽位置)
isSnapping = true;
const onTransitionEnd = () => {
launcherEl.removeEventListener('transitionend', onTransitionEnd);
launcherEl.classList.remove('csk-launcher--snap');
launcherEl.style.transition = '';
// 注意:不清除 style.bottom,保留用户拖拽的垂直位置
isSnapping = false;
};
launcherEl.addEventListener('transitionend', onTransitionEnd);
// 兜底:350ms 后强制清理(transitionend 可能不触发)
setTimeout(() => {
if (isSnapping)
onTransitionEnd();
}, 350);
// 恢复未读徽章
const badge = launcherEl.querySelector('.csk-launcher__badge');
if (badge)
badge.style.display = '';
if (onDragEnd) {
onDragEnd({ side: snapSide, bottom: clampedBottom });
}
}
// --- 鼠标事件 ---
function onMouseDown(e) { onPointerDown(e.clientX, e.clientY); }
function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); }
function onMouseUp() { onPointerUp(); }
// --- 触摸事件 ---
function onTouchStart(e) {
if (e.touches.length === 1)
onPointerDown(e.touches[0].clientX, e.touches[0].clientY);
}
function onTouchMove(e) {
if (e.touches.length === 1)
onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e);
}
function onTouchEnd() { onPointerUp(); }
launcherEl.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
launcherEl.addEventListener('touchstart', onTouchStart, { passive: true });
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
return () => {
launcherEl.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
launcherEl.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
}
// ==================== 消息渲染 ====================
/** 渲染用户消息气泡 */
function renderUserBubble(container, text, timestamp) {
@ -4188,6 +4356,9 @@ var ChatbotSDK = (function () {
let showLoadingFn = null;
let hideLoadingFn = null;
let dragCleanup = null;
let launcherDragCleanup = null;
/** Launcher 位置记忆状态(拖拽后更新) */
let launcherPos = null;
/** 提示气泡元素 */
let teaserEl = null;
/** 提示气泡定时器 */
@ -4223,6 +4394,15 @@ var ChatbotSDK = (function () {
document.body.appendChild(launcherEl);
// 获取未读徽章引用(由 createLauncher 创建)
badgeEl = launcherEl.querySelector('.csk-launcher__badge');
// 6.1 恢复 Launcher 位置(必须在挂载到 DOM 之后、拖拽绑定之前)
restoreLauncherPosition(config.integrateId, launcherEl);
// 6.2 启用 Launcher 拖拽(边缘吸附式,支持触摸 + 鼠标)
launcherDragCleanup = enableLauncherDrag(launcherEl, config, (pos) => {
launcherPos = pos;
saveLauncherPosition(config.integrateId, pos);
// Launcher 位置变更后,同步聊天弹窗对齐侧
syncWindowAlignment(pos.side);
});
// 7. 创建聊天弹窗
const dom = createChatWindow(config);
windowEl = dom.window;
@ -4327,15 +4507,21 @@ var ChatbotSDK = (function () {
function positionTeaser() {
if (!teaserEl || !launcherEl)
return;
// launcher 被拖拽过则用实际位置,否则用 DOM 实时计算
const rect = launcherEl.getBoundingClientRect();
const isLeft = (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom';
const isLeft = launcherPos ? launcherPos.side === 'left' : (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom';
if (isLeft) {
teaserEl.style.left = `${rect.left}px`;
teaserEl.style.right = 'auto';
}
else {
teaserEl.style.right = `${window.innerWidth - rect.right}px`;
teaserEl.style.left = 'auto';
}
teaserEl.style.bottom = `${window.innerHeight - rect.top + 8}px`;
// 同步 teaser 类名(箭头方向)
teaserEl.classList.toggle('csk-teaser--left', !!isLeft);
teaserEl.classList.toggle('csk-teaser--right', !isLeft);
}
function showTeaser() {
if (!teaserEl)
@ -4456,6 +4642,11 @@ var ChatbotSDK = (function () {
dragCleanup();
dragCleanup = null;
}
if (launcherDragCleanup) {
launcherDragCleanup();
launcherDragCleanup = null;
}
launcherPos = null;
removeStyles();
document.removeEventListener('keydown', onKeyDown);
window.removeEventListener('resize', onResize);
@ -4542,6 +4733,50 @@ var ChatbotSDK = (function () {
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
// ==================== Launcher 位置记忆 ====================
/** localStorage key:Launcher 位置 */
function launcherPosKey(integrateId) {
return `csk_launcher_pos_${integrateId}`;
}
/** 保存 Launcher 拖拽位置到 localStorage */
function saveLauncherPosition(integrateId, pos) {
try {
localStorage.setItem(launcherPosKey(integrateId), JSON.stringify(pos));
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
/** 恢复 Launcher 位置(初始化时调用) */
function restoreLauncherPosition(integrateId, el) {
try {
const raw = localStorage.getItem(launcherPosKey(integrateId));
if (!raw)
return;
const pos = JSON.parse(raw);
if ((pos.side !== 'left' && pos.side !== 'right') || typeof pos.bottom !== 'number')
return;
const maxBottom = window.innerHeight - 76;
const clampedBottom = Math.max(16, Math.min(pos.bottom, maxBottom));
// 覆盖默认对齐方向
el.classList.remove('csk-launcher--right', 'csk-launcher--left');
el.classList.add(`csk-launcher--${pos.side}`);
el.style.bottom = `${clampedBottom}px`;
// 保存到模块状态,供后续 positionTeaser / 弹窗对齐使用
launcherPos = { side: pos.side, bottom: clampedBottom };
}
catch ( /* 忽略 */_a) { /* 忽略 */ }
}
/** 同步聊天弹窗对齐侧(Launcher 拖拽到左/右侧时跟随) */
function syncWindowAlignment(side) {
if (!windowEl)
return;
windowEl.classList.remove('csk-window--right', 'csk-window--left');
windowEl.classList.add(`csk-window--${side}`);
// 同步 teaser 箭头方向
if (teaserEl) {
teaserEl.classList.remove('csk-teaser--right', 'csk-teaser--left');
teaserEl.classList.add(`csk-teaser--${side}`);
}
}
// ==================== 提示音 + 桌面通知 ====================
/** 播放短促提示音(Web Audio API 合成,无需音频文件) */
function playNotificationSound() {

2
src/main/resources/static/sdk/chatbot-sdk.js.map
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js
File diff suppressed because it is too large
View File

2
src/main/resources/static/sdk/chatbot-sdk.min.js.map
File diff suppressed because it is too large
View File

Loading…
Cancel
Save