| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- 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 = `<img src="${aiIcon}" width="24" height="24" alt="数据解读" />`;
- 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<typeof setTimeout> | null = null;
- let hideTimer: ReturnType<typeof setTimeout> | null = null;
- let leaveTimer: ReturnType<typeof setTimeout> | 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<typeof createHoverInstance>;
- // ─── 钩子主函数 ─────────────────────────────────────────────────────────────
- 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<string | null>(null);
- const thinking = ref(false);
- const todos = ref<TodoItem[]>([]);
- const executingTools = ref<string[]>([]);
- const toolCalls = ref<ToolCallRecord[]>([]);
- 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<Element, HoverInstance>();
- 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<Element, any>();
- const clickHandlerMap = new Map<Element, (e: Event) => 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 };
- }
|