@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
JavaScript
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
};