增加商品客服显示

This commit is contained in:
wenfp
2026-04-21 23:55:16 +08:00
parent 74572077e4
commit 1a5279a8f5
2 changed files with 432 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
<template>
<van-popover v-model="showPopover" trigger="click" placement="top-end">
<template #reference>
<button class="customer-service-btn">
<svg t="1776784019841" class="icon" viewBox="0 0 1051 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5796" width="20" height="20">
<path
d="M55.351351 553.402811v110.924108a83.027027 83.027027 0 0 0 166.054054 0v-110.924108a83.027027 83.027027 0 0 0-166.054054 0z m763.101406 211.552865A137.852541 137.852541 0 0 1 774.918919 664.326919v-110.924108A138.378378 138.378378 0 0 1 912.328649 415.135135C898.131027 214.071351 730.499459 55.351351 525.837838 55.351351 321.148541 55.351351 153.544649 214.071351 139.347027 415.135135A138.461405 138.461405 0 0 1 276.756757 553.402811v110.924108a138.378378 138.378378 0 0 1-276.756757 0v-110.924108a138.378378 138.378378 0 0 1 83.303784-126.865297C91.883243 189.523027 286.72 0 525.837838 0s433.954595 189.523027 442.534054 426.537514A138.461405 138.461405 0 0 1 1051.675676 553.402811v110.924108a138.378378 138.378378 0 0 1-184.790487 130.269405 470.763243 470.763243 0 0 1-188.858811 121.21946A96.809514 96.809514 0 0 1 580.912432 1010.162162h-82.528864c-53.690811 0-97.113946-43.174054-97.113946-96.864865 0-53.607784 43.284757-96.864865 97.141621-96.864865h82.473514c34.954378 0 65.536 18.265946 82.639567 45.803244a415.273514 415.273514 0 0 0 154.900757-97.28zM830.27027 553.402811v110.924108a83.027027 83.027027 0 0 0 166.054054 0v-110.924108a83.027027 83.027027 0 0 0-166.054054 0zM498.438919 954.810811h82.473513c23.302919 0 41.79027-18.487351 41.790271-41.513514 0-23.053838-18.570378-41.513514-41.790271-41.513513h-82.473513c-23.302919 0-41.79027 18.487351-41.79027 41.513513 0 23.053838 18.570378 41.513514 41.79027 41.513514z"
fill="#666666"
p-id="5797"
></path>
</svg>
<span>客服</span>
</button>
</template>
<div class="customer-group">
<p class="head">点击下方客服即可复制客服号码,可添加或添加微信号</p>
<div class="customer-type-group" v-for="(cus, index) in list" :key="index" @click="onService(cus)">
<img :src="cus.icon_url" alt="" />
<div class="info">
<h1>{{ cus.title }} <span class="wechat-account" v-copy="cus.value"></span></h1>
<p v-html="cus.content"></p>
</div>
<a class="phone" :href="'tel://' + cus.value" v-if="cus.title === '电话客服'"></a>
</div>
</div>
</van-popover>
</template>
<script>
import { getCustomerList } from "@/api";
export default {
data() {
return {
list: [],
showPopover: false,
actions: [{ name: "选项1" }, { name: "选项2" }, { name: "选项3" }],
};
},
watch: {
showPopover(val) {
console.log("val", val);
document.body.style.overflow = val ? "hidden" : "auto";
},
},
mounted() {
setTimeout(() => {
this.getCustomerList();
}, 1000);
},
methods: {
async getCustomerList() {
if (this.list.length) return;
const res = await getCustomerList();
this.list = res.list;
this.qrcode = this.$config.qrcode.wechatAccount || res.platform_code;
},
onService(cus) {
this.showPopover = false;
navigator.clipboard.writeText(cus.value);
this.$toast.success("已复制");
},
},
};
</script>
<style lang="scss" scoped>
.customer-service-btn {
width: 1.2rem;
height: 1.2rem;
background: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 8px;
font-size: 0.25rem;
gap: 0.14rem;
border: 1px solid #ddd;
}
.van-popover__content {
width: 90%;
}
.customer-group {
padding: 0.3rem;
width: 100%;
position: relative;
z-index: 10;
box-shadow: 0 -10px 10px rgba(0, 0, 0, 0.1);
.head {
font-size: 0.3rem;
color: #aaa;
padding: 0 0.4rem;
}
.customer-type-group {
display: flex;
background: white;
height: 1.67rem;
border-radius: 0.27rem;
padding: 0.2rem 0.4rem;
align-items: center;
position: relative;
img {
width: 1.07rem;
height: 1.07rem;
border-radius: 50%;
margin-right: 0.3rem;
}
&::after {
content: "";
width: 90%;
height: 1px;
background: #f1f1f1;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
}
&:last-child::after {
display: none;
}
.phone {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.info {
flex: 1;
font-size: 0.37rem;
h1 {
font-size: 0.4rem;
margin-bottom: 0.2rem;
.wechat-account {
color: rgb(167, 54, 54);
}
}
p {
color: #aaa;
font-size: 0.3rem;
line-height: 1.3;
}
}
}
}
</style>
+284
View File
@@ -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;