feat(bytedesk): 集成 Bytedesk 客服系统
新增 Bytedesk 在线客服功能,支持实时对话: 组件: - BytedeskWidget: 客服浮窗组件(右下角) - 配置文件: bytedesk.config.js 统一管理配置 - 环境变量示例: .env.bytedesk.example 集成方式: - GlobalComponents 引入 BytedeskWidget - public/index.html 加载 bytedesk-web.js 脚本 - 支持环境变量配置(ORG、SID、API_URL) 配置说明详见 src/bytedesk-integration/.env.bytedesk.example 🔧 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
177
src/bytedesk-integration/components/BytedeskWidget.jsx
Normal file
177
src/bytedesk-integration/components/BytedeskWidget.jsx
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Bytedesk客服Widget组件
|
||||
* 用于vf_react项目集成
|
||||
*
|
||||
* 使用方法:
|
||||
* import BytedeskWidget from './components/BytedeskWidget';
|
||||
* import { getBytedeskConfig } from './config/bytedesk.config';
|
||||
*
|
||||
* <BytedeskWidget
|
||||
* config={getBytedeskConfig()}
|
||||
* autoLoad={true}
|
||||
* />
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const BytedeskWidget = ({
|
||||
config,
|
||||
autoLoad = true,
|
||||
onLoad,
|
||||
onError
|
||||
}) => {
|
||||
const scriptRef = useRef(null);
|
||||
const widgetRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 如果不自动加载或配置未设置,跳过
|
||||
if (!autoLoad || !config) {
|
||||
if (!config) {
|
||||
console.warn('[Bytedesk] 配置未设置,客服组件未加载');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Bytedesk] 开始加载客服Widget...', config);
|
||||
|
||||
// 加载Bytedesk Widget脚本
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js';
|
||||
script.async = true;
|
||||
script.id = 'bytedesk-web-script';
|
||||
|
||||
script.onload = () => {
|
||||
console.log('[Bytedesk] Widget脚本加载成功');
|
||||
|
||||
try {
|
||||
if (window.BytedeskWeb) {
|
||||
console.log('[Bytedesk] 初始化Widget');
|
||||
const bytedesk = new window.BytedeskWeb(config);
|
||||
bytedesk.init();
|
||||
|
||||
widgetRef.current = bytedesk;
|
||||
console.log('[Bytedesk] Widget初始化成功');
|
||||
|
||||
if (onLoad) {
|
||||
onLoad(bytedesk);
|
||||
}
|
||||
} else {
|
||||
throw new Error('BytedeskWeb对象未定义');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Bytedesk] Widget初始化失败:', error);
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
script.onerror = (error) => {
|
||||
console.error('[Bytedesk] Widget脚本加载失败:', error);
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加脚本到页面
|
||||
document.body.appendChild(script);
|
||||
scriptRef.current = script;
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
console.log('[Bytedesk] 清理Widget');
|
||||
|
||||
try {
|
||||
// 调用Widget的destroy方法(如果存在)
|
||||
if (widgetRef.current && typeof widgetRef.current.destroy === 'function') {
|
||||
console.log('[Bytedesk] 调用Widget.destroy()');
|
||||
widgetRef.current.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Bytedesk] Widget.destroy()失败:', error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
// 移除脚本
|
||||
if (scriptRef.current) {
|
||||
if (document.body.contains(scriptRef.current)) {
|
||||
document.body.removeChild(scriptRef.current);
|
||||
}
|
||||
scriptRef.current = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Bytedesk] 移除脚本失败:', error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
// 移除Widget DOM元素(使用更安全的remove()方法)
|
||||
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
|
||||
widgetElements.forEach(el => {
|
||||
try {
|
||||
if (el && el.parentNode) {
|
||||
// 优先使用remove()方法(更现代、更安全)
|
||||
if (typeof el.remove === 'function') {
|
||||
el.remove();
|
||||
} else {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
} catch (removeError) {
|
||||
console.warn('[Bytedesk] 移除DOM元素失败:', el, removeError.message);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[Bytedesk] 清理DOM元素失败:', error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
// 清理全局对象
|
||||
if (window.BytedeskWeb) {
|
||||
delete window.BytedeskWeb;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Bytedesk] 清理全局对象失败:', error.message);
|
||||
}
|
||||
|
||||
console.log('[Bytedesk] Widget清理完成');
|
||||
};
|
||||
}, [config, autoLoad, onLoad, onError]);
|
||||
|
||||
// 不渲染任何元素(Widget会自动插入DOM到body)
|
||||
// 返回null避免React DOM管理冲突
|
||||
return null;
|
||||
};
|
||||
|
||||
BytedeskWidget.propTypes = {
|
||||
config: PropTypes.shape({
|
||||
apiUrl: PropTypes.string.isRequired,
|
||||
htmlUrl: PropTypes.string.isRequired,
|
||||
placement: PropTypes.oneOf(['bottom-right', 'bottom-left', 'top-right', 'top-left']),
|
||||
marginBottom: PropTypes.number,
|
||||
marginSide: PropTypes.number,
|
||||
autoPopup: PropTypes.bool,
|
||||
locale: PropTypes.string,
|
||||
bubbleConfig: PropTypes.shape({
|
||||
show: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
}),
|
||||
theme: PropTypes.shape({
|
||||
mode: PropTypes.oneOf(['light', 'dark', 'system']),
|
||||
backgroundColor: PropTypes.string,
|
||||
textColor: PropTypes.string,
|
||||
}),
|
||||
chatConfig: PropTypes.shape({
|
||||
org: PropTypes.string.isRequired,
|
||||
t: PropTypes.string.isRequired,
|
||||
sid: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
autoLoad: PropTypes.bool,
|
||||
onLoad: PropTypes.func,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
export default BytedeskWidget;
|
||||
Reference in New Issue
Block a user