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:
zdl
2025-11-10 17:26:42 +08:00
parent c593582006
commit 21564ebf4d
6 changed files with 801 additions and 0 deletions

View 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;