import { ref, h, createApp, Teleport, type Directive, type App } from 'vue'; import Antd from 'ant-design-vue'; import { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api'; import type { TodoItem, ToolCallRecord, SSERawEvent } from './types'; import DataPickerModal from './components/DataPickerModal.vue'; import aiIcon from '@/assets/images/ventAI/dataPicker/AI-icon.svg'; // ─── 按钮 DOM 构建(悬停模式专用)────────────────────────────────────────── const BUTTON_CLASS = 'di-floating-btn'; function createButtonEl(): HTMLElement { const btn = document.createElement('div'); btn.className = BUTTON_CLASS; btn.title = '数据解读'; btn.innerHTML = `数据解读`; Object.assign(btn.style, { position: 'fixed', zIndex: '99999', padding: '8px', background: 'linear-gradient(135deg, #1e90ff, #4da6ff)', borderRadius: '50%', cursor: 'pointer', display: 'none', pointerEvents: 'auto', transition: 'transform 0.15s ease-out, opacity 0.15s ease-out', transform: 'scale(0.9)', opacity: '0', boxShadow: '0 0 10px rgba(30,144,255,0.4)', }); btn.addEventListener('mouseenter', () => { const owner = (btn as any).__di_owner; if (owner) { owner.cancelLeave(); owner.clearHideTimer(); } }); btn.addEventListener('mouseleave', () => { const owner = (btn as any).__di_owner; if (owner) owner.startHideTimer(); }); return btn; } // ─── 悬停触发器实例 ──────────────────────────────────────────────────────── const PAUSE_DELAY = 400; const HIDE_DELAY = 3000; function createHoverInstance(btn: HTMLElement) { let pauseTimer: ReturnType | null = null; let hideTimer: ReturnType | null = null; let leaveTimer: ReturnType | null = null; let visible = false; let raw: any = null; let hostEl: HTMLElement | null = null; function setPosition(x: number, y: number) { btn.style.left = x + 'px'; btn.style.top = y + 'px'; } function detachMove() { if (hostEl && inst.onMove) hostEl.removeEventListener('mousemove', inst.onMove); } function attachMove() { if (hostEl && inst.onMove) hostEl.addEventListener('mousemove', inst.onMove); } function show() { clearPauseTimer(); detachMove(); visible = true; btn.style.display = 'block'; requestAnimationFrame(() => { btn.style.transform = 'scale(1)'; btn.style.opacity = '1'; }); startHideTimer(); } function hide() { visible = false; attachMove(); btn.style.display = 'none'; btn.style.transform = 'scale(0.9)'; btn.style.opacity = '0'; } function clearPauseTimer() { if (pauseTimer !== null) { clearTimeout(pauseTimer); pauseTimer = null; } } function clearHideTimer() { if (hideTimer !== null) { clearTimeout(hideTimer); hideTimer = null; } } function startHideTimer() { clearHideTimer(); hideTimer = setTimeout(hide, HIDE_DELAY); } const inst = { get visible() { return visible; }, get raw() { return raw; }, setHost(el: HTMLElement) { hostEl = el; }, onEnter(e: MouseEvent) { clearHideTimer(); if (!visible) { setPosition(e.clientX - 36, e.clientY - 34); clearPauseTimer(); pauseTimer = setTimeout(show, PAUSE_DELAY); } else { startHideTimer(); } }, onMove(e: MouseEvent) { setPosition(e.clientX - 36, e.clientY - 34); clearPauseTimer(); pauseTimer = setTimeout(show, PAUSE_DELAY); }, onLeave() { clearPauseTimer(); clearHideTimer(); leaveTimer = setTimeout(hide, 150); }, cancelLeave() { if (leaveTimer !== null) { clearTimeout(leaveTimer); leaveTimer = null; } }, setRaw(val: any) { raw = val; }, clearHideTimer, startHideTimer, destroy() { clearPauseTimer(); clearHideTimer(); if (leaveTimer !== null) { clearTimeout(leaveTimer); leaveTimer = null; } btn.remove(); }, }; return inst; } type HoverInstance = ReturnType; // ─── 钩子主函数 ───────────────────────────────────────────────────────────── export interface DataPickerOptions { /** hover: 悬停出现浮动按钮;click: 点击元素直接触发 */ mode?: 'hover' | 'click'; } export function useDataPicker( mapParams: ( item: any ) => { tun_id: string; tun_name: string; mode?: string } | { device_id: string; device_name: string; device_type: string; mode?: string }, options: DataPickerOptions = {} ) { console.log('useDataPicker', mapParams); const { mode = 'hover' } = options; const loading = ref(false); const streamingText = ref(''); const error = ref(null); const thinking = ref(false); const todos = ref([]); const executingTools = ref([]); const toolCalls = ref([]); const latestMessage = ref(''); const modalVisible = ref(false); const itemName = ref(''); // 弹框容器 + 独立 Vue 应用 const modalContainer = document.createElement('div'); Object.assign(modalContainer.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '1000', }); document.body.appendChild(modalContainer); let modalApp: App | null = null; function mountModal() { modalApp = createApp({ setup: () => ({ visible: modalVisible, loading, streamingText, error, thinking, todos, executingTools, toolCalls, latestMessage, itemName, onClose: () => { modalVisible.value = false; streamingText.value = ''; error.value = null; thinking.value = false; todos.value = []; executingTools.value = []; toolCalls.value = []; latestMessage.value = ''; }, }), render() { return h(Teleport, { to: 'body' }, [ h(DataPickerModal, { visible: this.visible, loading: this.loading, streamingText: this.streamingText, error: this.error, thinking: this.thinking, todos: this.todos, executingTools: this.executingTools, toolCalls: this.toolCalls, latestMessage: this.latestMessage, itemName: this.itemName, onClose: this.onClose, }), ]); }, }); modalApp.use(Antd); modalApp.mount(modalContainer); } mountModal(); // SSE 事件处理 function handleEvent(event: SSERawEvent) { switch (event.type) { case 'thinking': thinking.value = true; break; case 'executing': thinking.value = false; executingTools.value = event.tools || []; if (event.message) latestMessage.value = event.message; break; case 'tool_call': executingTools.value = []; toolCalls.value = [...toolCalls.value, { tool: event.tool || '', source: event.source || 'main', status: 'call' as const }]; if (event.message) latestMessage.value = event.message; break; case 'tool_result': if (toolCalls.value.length > 0) { const last = toolCalls.value[toolCalls.value.length - 1]; if (last.tool === event.tool && last.status === 'call') last.status = 'result'; } if (event.message) latestMessage.value = event.message; break; case 'updated_todo_list': thinking.value = false; todos.value = event.todos || []; break; case 'token': thinking.value = false; if (event.content) streamingText.value += event.content; break; } } // 点击触发 function handleClick(item: any) { modalVisible.value = true; loading.value = true; error.value = null; streamingText.value = ''; thinking.value = false; todos.value = []; executingTools.value = []; toolCalls.value = []; latestMessage.value = ''; const params = mapParams(item); itemName.value = 'tun_name' in params ? params.tun_name : params.device_name; const request = 'tun_id' in params ? fetchTunnelInterpretation(params, handleEvent) : fetchDeviceInterpretation(params, handleEvent); request .catch((e: any) => { error.value = e?.message || '请求失败'; }) .finally(() => { loading.value = false; thinking.value = false; executingTools.value = []; }); } // ── 悬停模式:按钮容器 + 触发器 ── let btnContainer: HTMLDivElement | null = null; const hoverMap = new Map(); if (mode === 'hover') { btnContainer = document.createElement('div'); Object.assign(btnContainer.style, { position: 'fixed', top: '0', left: '0', zIndex: '99999', pointerEvents: 'none' }); document.body.appendChild(btnContainer); } function createHoverTrigger(): HoverInstance { const btn = createButtonEl(); btnContainer!.appendChild(btn); const inst = createHoverInstance(btn); (btn as any).__di_owner = inst; btn.addEventListener('click', (e) => { e.stopPropagation(); const item = (inst as any).raw; if (item) handleClick(item); }); return inst; } // ── 点击模式:元素映射 ── const clickRawMap = new Map(); const clickHandlerMap = new Map void>(); // ── Vue 指令 ── const directive: Directive = { mounted(el, binding) { if (mode === 'hover') { const inst = createHoverTrigger(); inst.setHost(el); inst.setRaw(binding.value); hoverMap.set(el, inst); el.addEventListener('mouseenter', inst.onEnter); el.addEventListener('mousemove', inst.onMove); el.addEventListener('mouseleave', inst.onLeave); } else { clickRawMap.set(el, binding.value); const handler = (e: Event) => { e.stopPropagation(); const item = clickRawMap.get(el); if (item) handleClick(item); }; clickHandlerMap.set(el, handler); el.addEventListener('click', handler); } }, updated(el, binding) { if (mode === 'hover') { const inst = hoverMap.get(el); if (inst) inst.setRaw(binding.value); } else { clickRawMap.set(el, binding.value); } }, unmounted(el) { if (mode === 'hover') { const inst = hoverMap.get(el); if (inst) { inst.destroy(); hoverMap.delete(el); } } else { const handler = clickHandlerMap.get(el); if (handler) { el.removeEventListener('click', handler); clickHandlerMap.delete(el); } clickRawMap.delete(el); } }, }; function cleanup() { if (mode === 'hover') { hoverMap.forEach((inst) => inst.destroy()); hoverMap.clear(); if (btnContainer) { btnContainer.remove(); btnContainer = null; } } else { clickRawMap.clear(); clickHandlerMap.forEach((handler, el) => el.removeEventListener('click', handler)); clickHandlerMap.clear(); } if (modalApp) { modalApp.unmount(); modalApp = null; } modalContainer.remove(); } return { directive, cleanup, loading, streamingText, error, thinking, todos, executingTools, toolCalls, latestMessage, modalVisible }; }