UNPKG

@luckycode/vue-chat-button

Version:

Vue 3 chat button components with badge support, modal popup, fixed positioning, environment configuration, and customizable themes

1,024 lines (991 loc) 28.1 kB
import { defineComponent, ref, watch, onMounted, onUnmounted, createBlock, openBlock, Teleport, createVNode, Transition, withCtx, createElementBlock, createCommentVNode, createElementVNode, withModifiers, normalizeClass, renderSlot, createTextVNode, toDisplayString, computed, normalizeStyle } from "vue"; const detectEnvironment = () => { if (typeof window !== "undefined") { const hostname = window.location.hostname; const protocol = window.location.protocol; if (hostname === "localhost" || hostname === "127.0.0.1" || hostname.includes(".local") || hostname.includes(".dev") || protocol === "file:") { return "development"; } const urlParams = new URLSearchParams(window.location.search); if (urlParams.get("env") === "dev") { return "development"; } return "production"; } return "development"; }; const getEnvironmentConfig = (environment) => { const isProduction = environment === "production"; const defaultConfig = { isProduction, apiBaseUrl: isProduction ? "https://auth.szad.teehmoon.com" : "https://auth.dev.szad.teehmoon.com", timeout: 1e4, retryCount: 3 }; return { ...defaultConfig }; }; const _hoisted_1$2 = { class: "chat-modal-header" }; const _hoisted_2$2 = { class: "chat-modal-title" }; const _hoisted_3$2 = { class: "chat-modal-content" }; const _hoisted_4 = ["src", "title"]; const _hoisted_5 = { key: 1, class: "chat-modal-placeholder" }; const _sfc_main$2 = /* @__PURE__ */ defineComponent({ __name: "ChatModal", props: { visible: { type: Boolean, default: false }, iframeUrl: { default: "" }, modalTitle: { default: "在线客服" }, position: { default: "right" }, width: { default: "400px" }, height: { default: "600px" }, closeOnOverlay: { type: Boolean, default: true }, closeOnEscape: { type: Boolean, default: true } }, emits: ["update:visible", "close", "open"], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emit = __emit; const visible = ref(props.visible); watch( () => props.visible, (newValue) => { visible.value = newValue; } ); watch(visible, (newValue) => { emit("update:visible", newValue); if (newValue) { emit("open"); } else { emit("close"); } }); const closeModal = () => { visible.value = false; }; const handleOverlayClick = () => { if (props.closeOnOverlay) { closeModal(); } }; const handleKeydown = (event) => { if (event.key === "Escape" && visible.value && props.closeOnEscape) { event.preventDefault(); closeModal(); } }; __expose({ open: () => { visible.value = true; }, close: closeModal, toggle: () => { visible.value = !visible.value; } }); onMounted(() => { if (typeof document !== "undefined") { const styleId = "vue-chat-button-styles"; if (!document.getElementById(styleId)) { const style = document.createElement("style"); style.id = styleId; style.textContent = ` .chat-modal-overlay { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; background: rgb(0 0 0 / 50%); } .chat-modal-container { display: flex; flex-direction: column; overflow: hidden; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgb(0 0 0 / 15%); } .chat-modal-container.right { position: fixed; top: 0; right: 0; bottom: 0; width: var(--modal-width, 400px); height: 100vh; border-radius: 0; animation: slideInRight 0.3s ease-out; } .chat-modal-container.center { width: var(--modal-width, 400px); max-width: 90vw; height: var(--modal-height, 600px); max-height: 90vh; } .chat-modal-container.fullscreen { position: fixed; inset: 0; width: 100vw; height: 100vh; border-radius: 0; } .chat-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: #fafafa; border-bottom: 1px solid #e8e8e8; } .chat-modal-title { font-size: 16px; font-weight: 600; color: #333; } .chat-modal-close { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; color: #666; cursor: pointer; background: transparent; border: none; border-radius: 4px; transition: all 0.2s; } .chat-modal-close:hover { color: #333; background: #f0f0f0; } .chat-modal-close svg { width: 16px; height: 16px; } .chat-modal-content { flex: 1; overflow: hidden; } .chat-modal-iframe { width: 100%; height: 100%; border: none; } .chat-modal-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 14px; color: #999; } .modal-fade-enter-active, .modal-fade-leave-active { transition: opacity 0.3s ease; } .modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; } @keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } } @media (width <= 768px) { .chat-modal-container.right { width: 100vw; } .chat-modal-container.center { width: 95vw; height: 80vh; } } `; document.head.appendChild(style); } } document.addEventListener("keydown", handleKeydown, true); }); onUnmounted(() => { document.removeEventListener("keydown", handleKeydown, true); }); return (_ctx, _cache) => { return openBlock(), createBlock(Teleport, { to: "body" }, [ createVNode(Transition, { name: "modal-fade" }, { default: withCtx(() => [ visible.value ? (openBlock(), createElementBlock("div", { key: 0, class: "chat-modal-overlay", onClick: handleOverlayClick }, [ createElementVNode("div", { class: normalizeClass(["chat-modal-container", _ctx.position]), onClick: _cache[0] || (_cache[0] = withModifiers(() => { }, ["stop"])) }, [ createElementVNode("div", _hoisted_1$2, [ createElementVNode("div", _hoisted_2$2, [ renderSlot(_ctx.$slots, "title", {}, () => [ createTextVNode(toDisplayString(_ctx.modalTitle), 1) ]) ]), createElementVNode("button", { class: "chat-modal-close", onClick: closeModal }, _cache[1] || (_cache[1] = [ createElementVNode("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ createElementVNode("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), createElementVNode("line", { x1: "6", y1: "6", x2: "18", y2: "18" }) ], -1) ])) ]), createElementVNode("div", _hoisted_3$2, [ _ctx.iframeUrl ? (openBlock(), createElementBlock("iframe", { key: 0, src: _ctx.iframeUrl, title: _ctx.modalTitle, class: "chat-modal-iframe", frameborder: "0", allowfullscreen: "" }, null, 8, _hoisted_4)) : (openBlock(), createElementBlock("div", _hoisted_5, [ renderSlot(_ctx.$slots, "content", {}, () => [ _cache[2] || (_cache[2] = createElementVNode("p", null, "请设置 iframe URL", -1)) ]) ])) ]) ], 2) ])) : createCommentVNode("", true) ]), _: 3 }) ]); }; } }); const _hoisted_1$1 = { class: "chat-icon" }; const _hoisted_2$1 = ["src"]; const _hoisted_3$1 = { key: 1, viewBox: "0 0 24 24", fill: "none" }; const _sfc_main$1 = /* @__PURE__ */ defineComponent({ __name: "ChatButtonWithBadge", props: { autoFetch: { type: Boolean, default: true }, fetchInterval: { default: 3e4 }, enablePolling: { type: Boolean, default: true }, pollingInterval: { default: 3e4 }, params: { default: void 0 }, iconUrl: { default: "" }, themeColor: { default: "#1890ff" }, badgeColor: { default: "#ff4d4f" }, environment: { default: "development" }, modalUrl: { default: "https://admin.test.szad.teehmoon.com" }, modalTitle: { default: "在线客服" }, modalPosition: { default: "right" }, modalWidth: { default: "400px" }, modalHeight: { default: "600px" }, closeOnOverlay: { type: Boolean, default: true }, closeOnEscape: { type: Boolean, default: true }, autoOpenModal: { type: Boolean, default: true }, isFixed: { type: Boolean, default: true }, fixedPosition: { default: "bottom-right" }, fixedOffset: { default: () => ({ x: 20, y: 60 }) }, autoPosition: { type: Boolean, default: true }, mobilePosition: { default: "bottom-right" }, desktopPosition: { default: "bottom-right" } }, emits: ["click", "badgeUpdate", "pollingStart", "pollingStop", "error"], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emit = __emit; const badgeCount = ref(0); const isPolling = ref(false); const pollingTimer = ref(null); const currentInterval = ref(props.pollingInterval); const modalVisible = ref(false); const buttonStyle = computed(() => ({ background: props.themeColor, boxShadow: `0 4px 12px ${props.themeColor}30` })); const badgeStyle = computed(() => ({ background: props.badgeColor, boxShadow: `0 2px 4px ${props.badgeColor}30` })); const fixedStyle = computed(() => { if (!props.isFixed) return {}; const { x = 20, y = 20 } = props.fixedOffset || {}; let position = props.fixedPosition || "bottom-right"; if (props.autoPosition) { const isMobile = window.innerWidth <= 768; position = isMobile ? props.mobilePosition : props.desktopPosition; } const styles2 = { position: "fixed", zIndex: "9999" }; switch (position) { case "bottom-right": styles2.right = `${x}px`; styles2.bottom = `${y}px`; break; case "bottom-left": styles2.left = `${x}px`; styles2.bottom = `${y}px`; break; case "top-right": styles2.right = `${x}px`; styles2.top = `${y}px`; break; case "top-left": styles2.left = `${x}px`; styles2.top = `${y}px`; break; default: styles2.right = `${x}px`; styles2.bottom = `${y}px`; break; } return styles2; }); const handleClick = () => { emit("click"); if (props.autoOpenModal || props.modalUrl) { modalVisible.value = true; } }; const handleModalOpen = () => { console.log("Modal opened"); }; const handleModalClose = () => { console.log("Modal closed"); }; const fetchBadgeCount = async () => { try { const options = { method: "GET", headers: { "Content-Type": "application/json" }, body: props.params }; const envConfig = getEnvironmentConfig(props.environment); const apiUrl = `${envConfig.apiBaseUrl}/api/unread-count`; const response = await fetch(apiUrl, options); if (response.ok) { const data = await response.json(); const newCount = data.count || 0; badgeCount.value = newCount; emit("badgeUpdate", newCount); } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { emit("error", error); } }; const startPolling = () => { if (isPolling.value) return; isPolling.value = true; emit("pollingStart"); fetchBadgeCount(); pollingTimer.value = setInterval(fetchBadgeCount, currentInterval.value); }; const stopPolling = () => { if (pollingTimer.value) { clearInterval(pollingTimer.value); pollingTimer.value = null; } isPolling.value = false; emit("pollingStop"); }; const updatePollingInterval = (newInterval) => { if (isPolling.value) { stopPolling(); currentInterval.value = newInterval; startPolling(); } }; watch( () => props.enablePolling, (newValue) => { if (newValue && props.autoFetch) { startPolling(); } else { stopPolling(); } } ); watch( () => props.pollingInterval, (newValue) => { if (isPolling.value) { updatePollingInterval(newValue); } } ); onMounted(() => { if (typeof document !== "undefined") { const styleId = "vue-chat-button-styles"; if (!document.getElementById(styleId)) { const style = document.createElement("style"); style.id = styleId; style.textContent = ` .chat-button-with-badge { position: relative; display: inline-block; } .chat-button-with-badge.fixed-bottom-right { position: fixed; right: 20px; bottom: 20px; z-index: 9999; } @media (width <= 768px) { .chat-button-with-badge.fixed-bottom-right { right: 16px; bottom: 16px; } } .chat-button { position: relative; display: flex; align-items: center; justify-content: center; width: 56px; height: 56px; cursor: pointer; border-radius: 12px; transition: all 0.3s ease; } .chat-button:hover { box-shadow: 0 6px 16px rgb(24 144 255 / 40%) !important; transform: translateY(-2px); } .chat-button:active { transform: translateY(0); } .chat-icon { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; } .chat-icon svg { width: 20px; height: 20px; } .custom-icon { width: 20px; height: 20px; object-fit: contain; } .badge { position: absolute; top: -6px; right: -6px; display: flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; font-size: 12px; font-weight: 600; color: white; border-radius: 10px; animation: pulse 2s infinite; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } `; document.head.appendChild(style); } } if (props.autoFetch && props.enablePolling) { startPolling(); } else if (props.autoFetch) { fetchBadgeCount(); } }); onUnmounted(() => { stopPolling(); }); __expose({ fetchBadgeCount, setBadgeCount: (count) => { badgeCount.value = count; emit("badgeUpdate", count); }, startPolling, stopPolling, updatePollingInterval, isPolling: () => isPolling.value, // 弹窗相关方法 openModal: () => { modalVisible.value = true; }, closeModal: () => { modalVisible.value = false; }, toggleModal: () => { modalVisible.value = !modalVisible.value; } }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", { class: normalizeClass(["chat-button-with-badge", { "fixed-bottom-right": _ctx.isFixed }]), style: normalizeStyle(fixedStyle.value) }, [ createElementVNode("div", { class: "chat-button", style: normalizeStyle(buttonStyle.value), onClick: handleClick }, [ createElementVNode("div", _hoisted_1$1, [ _ctx.iconUrl ? (openBlock(), createElementBlock("img", { key: 0, src: _ctx.iconUrl, alt: "chat icon", class: "custom-icon" }, null, 8, _hoisted_2$1)) : (openBlock(), createElementBlock("svg", _hoisted_3$1, _cache[1] || (_cache[1] = [ createElementVNode("path", { d: "M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.17L4 17.17V4H20V16Z", fill: "white" }, null, -1), createElementVNode("circle", { cx: "7", cy: "9", r: "1", fill: "white" }, null, -1), createElementVNode("circle", { cx: "12", cy: "9", r: "1", fill: "white" }, null, -1), createElementVNode("circle", { cx: "17", cy: "9", r: "1", fill: "white" }, null, -1) ]))) ]), badgeCount.value > 0 ? (openBlock(), createElementBlock("div", { key: 0, class: "badge", style: normalizeStyle(badgeStyle.value) }, toDisplayString(badgeCount.value > 99 ? "99+" : badgeCount.value), 5)) : createCommentVNode("", true) ], 4), createVNode(_sfc_main$2, { visible: modalVisible.value, "onUpdate:visible": _cache[0] || (_cache[0] = ($event) => modalVisible.value = $event), "iframe-url": _ctx.modalUrl, "modal-title": _ctx.modalTitle, position: _ctx.modalPosition, width: _ctx.modalWidth, height: _ctx.modalHeight, "close-on-overlay": _ctx.closeOnOverlay, "close-on-escape": _ctx.closeOnEscape, onOpen: handleModalOpen, onClose: handleModalClose }, null, 8, ["visible", "iframe-url", "modal-title", "position", "width", "height", "close-on-overlay", "close-on-escape"]) ], 6); }; } }); const _hoisted_1 = { class: "chat-icon" }; const _hoisted_2 = ["src"]; const _hoisted_3 = { key: 1, viewBox: "0 0 24 24", fill: "none" }; const _sfc_main = /* @__PURE__ */ defineComponent({ __name: "ChatButtonSimple", props: { iconUrl: { default: "" }, themeColor: { default: "#1890ff" }, environment: { default: "development" } }, emits: ["click"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const buttonStyle = computed(() => ({ background: props.themeColor, boxShadow: `0 4px 12px ${props.themeColor}30` })); const handleClick = () => { emit("click"); }; onMounted(() => { if (typeof document !== "undefined") { const styleId = "vue-chat-button-styles"; if (!document.getElementById(styleId)) { const style = document.createElement("style"); style.id = styleId; style.textContent = ` .chat-button-simple { position: relative; display: inline-block; } .chat-button { position: relative; display: flex; align-items: center; justify-content: center; width: 56px; height: 56px; cursor: pointer; border-radius: 12px; transition: all 0.3s ease; } .chat-button:hover { box-shadow: 0 6px 16px rgb(24 144 255 / 40%) !important; transform: translateY(-2px); } .chat-button:active { transform: translateY(0); } .chat-icon { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; } .chat-icon svg { width: 20px; height: 20px; } .custom-icon { width: 20px; height: 20px; object-fit: contain; } `; document.head.appendChild(style); } } }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", { class: "chat-button-simple", onClick: handleClick }, [ createElementVNode("div", { class: "chat-button", style: normalizeStyle(buttonStyle.value) }, [ createElementVNode("div", _hoisted_1, [ _ctx.iconUrl ? (openBlock(), createElementBlock("img", { key: 0, src: _ctx.iconUrl, alt: "chat icon", class: "custom-icon" }, null, 8, _hoisted_2)) : (openBlock(), createElementBlock("svg", _hoisted_3, _cache[0] || (_cache[0] = [ createElementVNode("path", { d: "M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.17L4 17.17V4H20V16Z", fill: "white" }, null, -1), createElementVNode("circle", { cx: "7", cy: "9", r: "1", fill: "white" }, null, -1), createElementVNode("circle", { cx: "12", cy: "9", r: "1", fill: "white" }, null, -1), createElementVNode("circle", { cx: "17", cy: "9", r: "1", fill: "white" }, null, -1) ]))) ]) ], 4) ]); }; } }); const styles = ` .chat-button-simple { position: relative; display: inline-block; } .chat-button { position: relative; display: flex; align-items: center; justify-content: center; width: 56px; height: 56px; cursor: pointer; border-radius: 12px; transition: all 0.3s ease; } .chat-button:hover { box-shadow: 0 6px 16px rgb(24 144 255 / 40%) !important; transform: translateY(-2px); } .chat-button:active { transform: translateY(0); } .chat-icon { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; } .chat-icon svg { width: 20px; height: 20px; } .custom-icon { width: 20px; height: 20px; object-fit: contain; } .chat-button-with-badge { position: relative; display: inline-block; } .chat-button-with-badge.fixed-bottom-right { position: fixed; right: 20px; bottom: 20px; z-index: 9999; } @media (width <= 768px) { .chat-button-with-badge.fixed-bottom-right { right: 16px; bottom: 16px; } } .badge { position: absolute; top: -6px; right: -6px; display: flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; font-size: 12px; font-weight: 600; color: white; border-radius: 10px; animation: pulse 2s infinite; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } .chat-modal-overlay { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; background: rgb(0 0 0 / 50%); } .chat-modal-container { display: flex; flex-direction: column; overflow: hidden; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgb(0 0 0 / 15%); } .chat-modal-container.right { position: fixed; top: 0; right: 0; bottom: 0; width: var(--modal-width, 400px); height: 100vh; border-radius: 0; animation: slideInRight 0.3s ease-out; } .chat-modal-container.center { width: var(--modal-width, 400px); max-width: 90vw; height: var(--modal-height, 600px); max-height: 90vh; } .chat-modal-container.fullscreen { position: fixed; inset: 0; width: 100vw; height: 100vh; border-radius: 0; } .chat-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: #fafafa; border-bottom: 1px solid #e8e8e8; } .chat-modal-title { font-size: 16px; font-weight: 600; color: #333; } .chat-modal-close { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; color: #666; cursor: pointer; background: transparent; border: none; border-radius: 4px; transition: all 0.2s; } .chat-modal-close:hover { color: #333; background: #f0f0f0; } .chat-modal-close svg { width: 16px; height: 16px; } .chat-modal-content { flex: 1; overflow: hidden; } .chat-modal-iframe { width: 100%; height: 100%; border: none; } .chat-modal-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 14px; color: #999; } .modal-fade-enter-active, .modal-fade-leave-active { transition: opacity 0.3s ease; } .modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; } @keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } } @media (width <= 768px) { .chat-modal-container.right { width: 100vw; } .chat-modal-container.center { width: 95vw; height: 80vh; } } `; function injectStyles() { try { if (typeof document !== "undefined") { const styleId = "vue-chat-button-styles"; if (!document.getElementById(styleId)) { const style = document.createElement("style"); style.id = styleId; style.textContent = styles; document.head.appendChild(style); console.log("Vue Chat Button styles injected successfully"); } else { console.log("Vue Chat Button styles already injected"); } } } catch (error) { console.warn("Failed to inject Vue Chat Button styles:", error); } } const VueChatButton = { install(app) { injectStyles(); app.component("ChatButtonWithBadge", _sfc_main$1); app.component("ChatButtonSimple", _sfc_main); app.component("ChatModal", _sfc_main$2); if (app._context && app._context.provides) { app.provide("vue-chat-button-styles-injected", true); } } }; export { _sfc_main as ChatButtonSimple, _sfc_main$1 as ChatButtonWithBadge, _sfc_main$2 as ChatModal, VueChatButton as default, detectEnvironment, getEnvironmentConfig };