From 1a5279a8f5f29b45243d6dfda22235aca04d7ee0 Mon Sep 17 00:00:00 2001 From: wenfp Date: Tue, 21 Apr 2026 23:55:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=95=86=E5=93=81=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/customer-service/index.vue | 148 +++++++++++ src/util/ObserverInteraction.js | 284 ++++++++++++++++++++++ 2 files changed, 432 insertions(+) create mode 100644 src/components/customer-service/index.vue create mode 100644 src/util/ObserverInteraction.js diff --git a/src/components/customer-service/index.vue b/src/components/customer-service/index.vue new file mode 100644 index 0000000..e944a6a --- /dev/null +++ b/src/components/customer-service/index.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/src/util/ObserverInteraction.js b/src/util/ObserverInteraction.js new file mode 100644 index 0000000..a3a5346 --- /dev/null +++ b/src/util/ObserverInteraction.js @@ -0,0 +1,284 @@ +// utils/ObserverInteraction.js + +class ObserverInteraction { + constructor(options = {}) { + this.options = { + attributes: true, // 观察属性变化 + childList: false, // 观察子节点变化 + subtree: false, // 观察后代节点 + attributeFilter: [], // 观察特定属性 + attributeOldValue: false, // 记录属性旧值 + ...options, + }; + + this.observer = null; + this.callbacks = new Map(); + this.targets = new Set(); + } + + /** + * 开始观察目标元素 + * @param {HTMLElement} target - 要观察的DOM元素 + * @param {Function} callback - 变化时的回调函数 + * @param {Object} customOptions - 自定义观察选项 + */ + observe(target, callback, customOptions = {}) { + if (!target) { + console.error("ObserverInteraction: 目标元素不存在"); + return; + } + + const options = { ...this.options, ...customOptions }; + + // 如果已存在观察器,先断开 + if (this.observer) { + this.disconnect(); + } + + // 创建新的观察器 + this.observer = new MutationObserver((mutationsList) => { + const changes = this._processMutations(mutationsList); + if (changes.length > 0 && callback) { + callback(changes, target); + } + + // 触发所有注册的回调 + this.callbacks.forEach((cb, element) => { + if (element === target || element.contains(target)) { + cb(changes, target); + } + }); + }); + + // 开始观察 + this.observer.observe(target, options); + this.targets.add(target); + + // 存储回调 + if (callback) { + this.callbacks.set(target, callback); + } + + return this; + } + + /** + * 监听特定属性的变化 + * @param {HTMLElement} target - 目标元素 + * @param {string} attributeName - 属性名 + * @param {Function} callback - 回调函数 + */ + watchAttribute(target, attributeName, callback) { + return this.observe(target, callback, { + attributes: true, + attributeFilter: [attributeName], + attributeOldValue: true, + }); + } + + /** + * 监听元素大小变化(使用 ResizeObserver) + * @param {HTMLElement} target - 目标元素 + * @param {Function} callback - 回调函数 + */ + observeResize(target, callback) { + if (!window.ResizeObserver) { + console.warn("ResizeObserver 不支持当前浏览器"); + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + callback({ width, height, target: entry.target }); + } + }); + + resizeObserver.observe(target); + + // 存储以便清理 + this.resizeObservers = this.resizeObservers || new Map(); + this.resizeObservers.set(target, resizeObserver); + + return () => this.unobserveResize(target); + } + + /** + * 处理 mutations 数据 + * @private + */ + _processMutations(mutationsList) { + const changes = []; + + for (const mutation of mutationsList) { + const change = { + type: mutation.type, + target: mutation.target, + timestamp: Date.now(), + }; + + if (mutation.type === "attributes") { + change.attributeName = mutation.attributeName; + change.oldValue = mutation.oldValue; + change.newValue = mutation.target.getAttribute(mutation.attributeName); + } + + if (mutation.type === "childList") { + change.addedNodes = Array.from(mutation.addedNodes); + change.removedNodes = Array.from(mutation.removedNodes); + } + + changes.push(change); + } + + return changes; + } + + /** + * 注册交互事件 + * @param {HTMLElement} element - 元素 + * @param {string} eventType - 事件类型 + * @param {Function} callback - 回调 + */ + on(element, eventType, callback) { + if (!element || !callback) return; + + const handler = (event) => { + callback(event, element); + }; + + element.addEventListener(eventType, handler); + + // 存储以便清理 + this.eventHandlers = this.eventHandlers || new Map(); + if (!this.eventHandlers.has(element)) { + this.eventHandlers.set(element, new Map()); + } + this.eventHandlers.get(element).set(eventType, handler); + + return () => this.off(element, eventType); + } + + /** + * 移除事件监听 + * @param {HTMLElement} element - 元素 + * @param {string} eventType - 事件类型 + */ + off(element, eventType) { + if (!this.eventHandlers?.has(element)) return; + + const handlers = this.eventHandlers.get(element); + if (eventType) { + const handler = handlers.get(eventType); + if (handler) { + element.removeEventListener(eventType, handler); + handlers.delete(eventType); + } + } else { + handlers.forEach((handler, type) => { + element.removeEventListener(type, handler); + }); + this.eventHandlers.delete(element); + } + } + + /** + * 观察元素可见性(Intersection Observer) + * @param {HTMLElement} target - 目标元素 + * @param {Function} callback - 可见性变化回调 + * @param {Object} options - IntersectionObserver 配置 + */ + observeVisibility(target, callback, options = {}) { + const defaultOptions = { + threshold: 0.1, + rootMargin: "0px", + }; + + const intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + callback({ + isIntersecting: entry.isIntersecting, + intersectionRatio: entry.intersectionRatio, + boundingClientRect: entry.boundingClientRect, + target: entry.target, + }); + }); + }, + { ...defaultOptions, ...options } + ); + + intersectionObserver.observe(target); + + this.intersectionObservers = this.intersectionObservers || new Map(); + this.intersectionObservers.set(target, intersectionObserver); + + return () => this.unobserveVisibility(target); + } + + /** + * 停止观察可见性 + * @param {HTMLElement} target - 目标元素 + */ + unobserveVisibility(target) { + const observer = this.intersectionObservers?.get(target); + if (observer) { + observer.unobserve(target); + this.intersectionObservers.delete(target); + } + } + + /** + * 停止观察大小变化 + * @param {HTMLElement} target - 目标元素 + */ + unobserveResize(target) { + const observer = this.resizeObservers?.get(target); + if (observer) { + observer.unobserve(target); + this.resizeObservers.delete(target); + } + } + + /** + * 断开所有观察器 + */ + disconnect() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + + // 清理 ResizeObservers + this.resizeObservers?.forEach((observer) => { + observer.disconnect(); + }); + this.resizeObservers?.clear(); + + // 清理 IntersectionObservers + this.intersectionObservers?.forEach((observer) => { + observer.disconnect(); + }); + this.intersectionObservers?.clear(); + + this.targets.clear(); + this.callbacks.clear(); + } + + /** + * 销毁实例 + */ + destroy() { + this.disconnect(); + + // 清理所有事件监听 + this.eventHandlers?.forEach((handlers, element) => { + handlers.forEach((handler, type) => { + element.removeEventListener(type, handler); + }); + }); + this.eventHandlers?.clear(); + } +} + +export default ObserverInteraction;