/** * DOM 构建模块 - 悬浮按钮 + 聊天弹窗容器 */ import { ResolvedConfig } from './types'; import { debounce } from './utils'; // ==================== 悬浮按钮 ==================== /** 创建悬浮按钮 */ export function createLauncher(config: ResolvedConfig, onClick: () => void): HTMLElement { const launcher = document.createElement('div'); launcher.id = 'csk-launcher'; launcher.className = `csk-launcher csk-launcher--${config.position === 'left-bottom' ? 'left' : 'right'}`; launcher.setAttribute('title', config.title); launcher.setAttribute('aria-label', config.title); launcher.setAttribute('role', 'button'); launcher.setAttribute('tabindex', '0'); // 图标内容 launcher.innerHTML = config.launcherIcon; // 点击事件(300ms 防抖) const debouncedClick = debounce(onClick, 300); launcher.addEventListener('click', debouncedClick); // 键盘支持 launcher.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); debouncedClick(); } }); return launcher; } // ==================== 聊天弹窗 ==================== /** 创建聊天弹窗完整结构,返回各区域引用 */ export function createChatWindow(config: ResolvedConfig): { window: HTMLElement; messagesContainer: HTMLElement; inputEl: HTMLTextAreaElement; sendBtn: HTMLElement; clearBtn: HTMLElement | null; showLoading: () => HTMLElement; hideLoading: () => void; } { // 最外层容器 const windowEl = document.createElement('div'); windowEl.id = 'csk-window'; windowEl.className = `csk-root csk-window csk-window--${config.position === 'left-bottom' ? 'left' : 'right'} csk-window--hidden`; // === 头部 === const header = document.createElement('div'); header.className = 'csk-header'; const titleEl = document.createElement('span'); titleEl.className = 'csk-header__title'; titleEl.textContent = config.title; const actions = document.createElement('div'); actions.className = 'csk-header__actions'; // 最小化按钮 const minimizeBtn = document.createElement('button'); minimizeBtn.className = 'csk-header__btn csk-header__btn--minimize'; minimizeBtn.setAttribute('title', '最小化'); minimizeBtn.innerHTML = ``; minimizeBtn.addEventListener('click', () => { windowEl.classList.add('csk-window--hidden'); }); // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.className = 'csk-header__btn csk-header__btn--close'; closeBtn.setAttribute('title', '关闭'); closeBtn.innerHTML = ``; closeBtn.addEventListener('click', () => { windowEl.classList.add('csk-window--hidden'); }); actions.appendChild(minimizeBtn); actions.appendChild(closeBtn); header.appendChild(titleEl); header.appendChild(actions); // === 消息区 === const messagesContainer = document.createElement('div'); messagesContainer.id = 'csk-messages'; messagesContainer.className = 'csk-messages'; // === 输入区 === const inputArea = document.createElement('div'); inputArea.className = 'csk-input-area'; const inputEl = document.createElement('textarea'); inputEl.id = 'csk-input'; inputEl.className = 'csk-input'; inputEl.setAttribute('placeholder', '输入您的问题...'); inputEl.setAttribute('rows', '1'); inputEl.setAttribute('autofocus', ''); const sendBtn = document.createElement('button'); sendBtn.id = 'csk-send-btn'; sendBtn.className = 'csk-send-btn'; sendBtn.setAttribute('title', '发送'); sendBtn.setAttribute('disabled', 'true'); sendBtn.innerHTML = ``; inputArea.appendChild(inputEl); inputArea.appendChild(sendBtn); // === 组装 === windowEl.appendChild(header); windowEl.appendChild(messagesContainer); windowEl.appendChild(inputArea); // 清空按钮(可选) let clearBtn: HTMLElement | null = null; if (config.showClear) { clearBtn = document.createElement('button'); clearBtn.className = 'csk-clear-btn'; clearBtn.textContent = '清空对话'; clearBtn.style.display = 'none'; // 初始隐藏,有消息后才显示 // 插入到 messages 之后、inputArea 之前 windowEl.insertBefore(clearBtn, inputArea); } // === Loading 动画 === let loadingEl: HTMLElement | null = null; function showLoading(): HTMLElement { if (loadingEl) { loadingEl.style.display = 'flex'; return loadingEl; } const el = document.createElement('div'); el.className = 'csk-loading'; el.innerHTML = `
`; messagesContainer.appendChild(el); loadingEl = el; return el; } function hideLoading(): void { if (loadingEl && loadingEl.parentNode) { loadingEl.parentNode.removeChild(loadingEl); loadingEl = null; } } return { window: windowEl, messagesContainer, inputEl, sendBtn, clearBtn, showLoading, hideLoading, }; } // ==================== 拖拽支持 ==================== /** 启用弹窗拖拽 */ export function enableDrag(headerEl: HTMLElement, windowEl: HTMLElement): () => void { let dragging = false; let startX = 0; let startY = 0; let offsetX = 0; let offsetY = 0; const onMouseDown = (e: MouseEvent) => { dragging = true; startX = e.clientX; startY = e.clientY; const rect = windowEl.getBoundingClientRect(); offsetX = startX - rect.left; offsetY = startY - rect.top; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; const onMouseMove = (e: MouseEvent) => { if (!dragging) return; const x = e.clientX - offsetX; const y = e.clientY - offsetY; // 边界限制,防止拖出视口 const maxX = window.innerWidth - windowEl.offsetWidth; const maxY = window.innerHeight - windowEl.offsetHeight; windowEl.style.right = 'auto'; windowEl.style.bottom = 'auto'; windowEl.style.left = `${Math.max(0, Math.min(x, maxX))}px`; windowEl.style.top = `${Math.max(0, Math.min(y, maxY))}px`; }; const onMouseUp = () => { dragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; headerEl.addEventListener('mousedown', onMouseDown); // 清理函数 return () => { headerEl.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; } // ==================== 消息渲染 ==================== /** 渲染用户消息气泡 */ export function renderUserBubble(container: HTMLElement, text: string, timestamp: number): HTMLElement { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--user'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.textContent = text; const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); wrapper.appendChild(bubble); wrapper.appendChild(time); container.appendChild(wrapper); return wrapper; } /** 渲染 AI 消息气泡 */ export function renderAIBubble(container: HTMLElement, text: string, timestamp: number): HTMLElement { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--ai'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.textContent = text; const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); wrapper.appendChild(bubble); wrapper.appendChild(time); container.appendChild(wrapper); return wrapper; } /** 创建空的 AI 气泡(流式追加用) */ export function createEmptyAIBubble(container: HTMLElement, timestamp: number): { wrapper: HTMLElement; bubble: HTMLElement } { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--ai'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.innerHTML = ''; const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); wrapper.appendChild(bubble); wrapper.appendChild(time); container.appendChild(wrapper); return { wrapper, bubble }; } /** 滚动消息区到底部 */ export function scrollToBottom(container: HTMLElement): void { container.scrollTop = container.scrollHeight; } /** 格式化时间戳 */ function formatTime(timestamp: number): string { const d = new Date(timestamp); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; }