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:
@@ -84,6 +84,90 @@
|
|||||||
// name: 'YOU CAN DEFINE USER NAME HERE',
|
// name: 'YOU CAN DEFINE USER NAME HERE',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据路径控制Dify机器人显示(只在首页/和home页/home显示)
|
||||||
|
function controlDifyChatbot() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const chatbotButton = document.getElementById('dify-chatbot-bubble-button');
|
||||||
|
const chatbotWindow = document.getElementById('dify-chatbot-bubble-window');
|
||||||
|
|
||||||
|
// 只在首页(/)和home页(/home)显示Dify机器人
|
||||||
|
// const shouldShowDify = (path === '/' || path === '/home');
|
||||||
|
// 完全不显示Dify机器人(只使用Bytedesk客服)
|
||||||
|
const shouldShowDify = false
|
||||||
|
|
||||||
|
if (chatbotButton) {
|
||||||
|
chatbotButton.style.display = shouldShowDify ? 'block' : 'none';
|
||||||
|
// 同时设置visibility确保完全隐藏
|
||||||
|
chatbotButton.style.visibility = shouldShowDify ? 'visible' : 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatbotWindow) {
|
||||||
|
chatbotWindow.style.display = shouldShowDify ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Dify] Path:', path, 'Should show:', shouldShowDify, 'Button found:', !!chatbotButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询检查Dify按钮(因为Dify脚本加载是异步的)
|
||||||
|
let difyCheckCount = 0;
|
||||||
|
const difyCheckInterval = setInterval(function() {
|
||||||
|
const button = document.getElementById('dify-chatbot-bubble-button');
|
||||||
|
if (button || difyCheckCount > 50) { // 最多检查5秒
|
||||||
|
if (button) {
|
||||||
|
console.log('[Dify] Button found, applying control');
|
||||||
|
controlDifyChatbot();
|
||||||
|
}
|
||||||
|
clearInterval(difyCheckInterval);
|
||||||
|
}
|
||||||
|
difyCheckCount++;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 页面加载时执行
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(controlDifyChatbot, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听路由变化(React Router使用pushState)
|
||||||
|
window.addEventListener('popstate', controlDifyChatbot);
|
||||||
|
|
||||||
|
// 监听pushState和replaceState(捕获React Router导航)
|
||||||
|
const originalPushState = history.pushState;
|
||||||
|
const originalReplaceState = history.replaceState;
|
||||||
|
|
||||||
|
history.pushState = function() {
|
||||||
|
originalPushState.apply(history, arguments);
|
||||||
|
setTimeout(controlDifyChatbot, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
history.replaceState = function() {
|
||||||
|
originalReplaceState.apply(history, arguments);
|
||||||
|
setTimeout(controlDifyChatbot, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用MutationObserver监听DOM变化(捕获Dify按钮插入)
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.addedNodes.length > 0) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.id && (node.id.includes('dify') || node.id.includes('chatbot'))) {
|
||||||
|
console.log('[Dify] Detected chatbot element insertion:', node.id);
|
||||||
|
setTimeout(controlDifyChatbot, 100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 观察body的变化
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: false
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<script
|
<script
|
||||||
src="https://app.valuefrontier.cn/embed.min.js"
|
src="https://app.valuefrontier.cn/embed.min.js"
|
||||||
|
|||||||
156
src/bytedesk-integration/.env.bytedesk.example
Normal file
156
src/bytedesk-integration/.env.bytedesk.example
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
################################################################################
|
||||||
|
# Bytedesk客服系统环境变量配置示例
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# 1. 复制本文件到vf_react项目根目录(与package.json同级)
|
||||||
|
# cp bytedesk-integration/.env.bytedesk.example .env.local
|
||||||
|
#
|
||||||
|
# 2. 根据实际部署环境修改配置值
|
||||||
|
#
|
||||||
|
# 3. 重启开发服务器使配置生效
|
||||||
|
# npm start
|
||||||
|
#
|
||||||
|
# 注意事项:
|
||||||
|
# - .env.local文件不应提交到Git(已在.gitignore中)
|
||||||
|
# - 开发环境和生产环境应使用不同的配置文件
|
||||||
|
# - 所有以REACT_APP_开头的变量会被打包到前端代码中
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Bytedesk服务器配置(必需)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Bytedesk后端服务地址(生产环境)
|
||||||
|
# 格式: http://IP地址 或 https://域名
|
||||||
|
# 示例: http://43.143.189.195 或 https://kefu.yourdomain.com
|
||||||
|
REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Bytedesk组织和工作组配置(必需)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 组织ID(Organization UID)
|
||||||
|
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 复制UID
|
||||||
|
# 示例: df_org_uid
|
||||||
|
REACT_APP_BYTEDESK_ORG=df_org_uid
|
||||||
|
|
||||||
|
# 工作组ID(Workgroup SID)
|
||||||
|
# 获取方式: 登录管理后台 -> 客服管理 -> 工作组 -> 复制工作组ID
|
||||||
|
# 示例: df_wg_aftersales (售后服务组)
|
||||||
|
REACT_APP_BYTEDESK_SID=df_wg_aftersales
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 可选配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 客服类型
|
||||||
|
# 2 = 人工客服(默认)
|
||||||
|
# 1 = 机器人客服
|
||||||
|
# REACT_APP_BYTEDESK_TYPE=2
|
||||||
|
|
||||||
|
# 语言设置
|
||||||
|
# zh-cn = 简体中文(默认)
|
||||||
|
# en = 英语
|
||||||
|
# ja = 日语
|
||||||
|
# ko = 韩语
|
||||||
|
# REACT_APP_BYTEDESK_LOCALE=zh-cn
|
||||||
|
|
||||||
|
# 客服图标位置
|
||||||
|
# bottom-right = 右下角(默认)
|
||||||
|
# bottom-left = 左下角
|
||||||
|
# top-right = 右上角
|
||||||
|
# top-left = 左上角
|
||||||
|
# REACT_APP_BYTEDESK_PLACEMENT=bottom-right
|
||||||
|
|
||||||
|
# 客服图标边距(像素)
|
||||||
|
# REACT_APP_BYTEDESK_MARGIN_BOTTOM=20
|
||||||
|
# REACT_APP_BYTEDESK_MARGIN_SIDE=20
|
||||||
|
|
||||||
|
# 主题模式
|
||||||
|
# system = 跟随系统(默认)
|
||||||
|
# light = 亮色模式
|
||||||
|
# dark = 暗色模式
|
||||||
|
# REACT_APP_BYTEDESK_THEME_MODE=system
|
||||||
|
|
||||||
|
# 主题色(十六进制颜色)
|
||||||
|
# REACT_APP_BYTEDESK_THEME_COLOR=#0066FF
|
||||||
|
|
||||||
|
# 是否自动弹出客服窗口(不推荐)
|
||||||
|
# true = 页面加载后自动弹出
|
||||||
|
# false = 需用户点击图标弹出(默认)
|
||||||
|
# REACT_APP_BYTEDESK_AUTO_POPUP=false
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 开发环境专用配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 开发环境可以使用不同的服务器地址
|
||||||
|
# 取消注释以下行使用本地或测试服务器
|
||||||
|
# REACT_APP_BYTEDESK_API_URL_DEV=http://localhost:9003
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 配置示例 - 不同部署场景
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ---------- 示例1: 生产环境(域名访问) ----------
|
||||||
|
# REACT_APP_BYTEDESK_API_URL=https://kefu.yourdomain.com
|
||||||
|
# REACT_APP_BYTEDESK_ORG=prod_org_12345
|
||||||
|
# REACT_APP_BYTEDESK_SID=prod_wg_sales
|
||||||
|
|
||||||
|
# ---------- 示例2: 测试环境(IP访问) ----------
|
||||||
|
# REACT_APP_BYTEDESK_API_URL=http://192.168.1.100
|
||||||
|
# REACT_APP_BYTEDESK_ORG=test_org_abc
|
||||||
|
# REACT_APP_BYTEDESK_SID=test_wg_support
|
||||||
|
|
||||||
|
# ---------- 示例3: 本地开发环境 ----------
|
||||||
|
# REACT_APP_BYTEDESK_API_URL=http://localhost:9003
|
||||||
|
# REACT_APP_BYTEDESK_ORG=dev_org_local
|
||||||
|
# REACT_APP_BYTEDESK_SID=dev_wg_test
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 故障排查
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 问题1: 客服图标不显示
|
||||||
|
# 解决方案:
|
||||||
|
# - 检查REACT_APP_BYTEDESK_API_URL是否可访问
|
||||||
|
# - 确认.env文件在项目根目录
|
||||||
|
# - 重启开发服务器(npm start)
|
||||||
|
# - 查看浏览器控制台是否有错误
|
||||||
|
|
||||||
|
# 问题2: 连接不上后端服务
|
||||||
|
# 解决方案:
|
||||||
|
# - 确认后端服务已启动(docker ps查看bytedesk-prod容器)
|
||||||
|
# - 检查CORS配置(后端.env.production中的BYTEDESK_CORS_ALLOWED_ORIGINS)
|
||||||
|
# - 确认防火墙未阻止80/443端口
|
||||||
|
|
||||||
|
# 问题3: ORG或SID配置错误
|
||||||
|
# 解决方案:
|
||||||
|
# - 登录管理后台http://43.143.189.195/admin/
|
||||||
|
# - 导航到"设置" -> "组织信息"获取ORG
|
||||||
|
# - 导航到"客服管理" -> "工作组"获取SID
|
||||||
|
# - 确保复制的ID没有多余空格
|
||||||
|
|
||||||
|
# 问题4: 多工作组场景
|
||||||
|
# 解决方案:
|
||||||
|
# - 可以为不同页面配置不同的SID
|
||||||
|
# - 在bytedesk.config.js中使用条件判断
|
||||||
|
# - 示例: 售后页面用售后组SID,销售页面用销售组SID
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 安全提示
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 1. 不要在代码中硬编码API地址和ID
|
||||||
|
# 2. .env.local文件不应提交到Git仓库
|
||||||
|
# 3. 生产环境建议使用HTTPS
|
||||||
|
# 4. 定期更新后端服务器的安全补丁
|
||||||
|
# 5. 不要在公开的代码库中暴露组织ID和工作组ID
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 更多信息
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Bytedesk官方文档: https://docs.bytedesk.com
|
||||||
|
# 技术支持: 访问http://43.143.189.195/chat/联系在线客服
|
||||||
|
# GitHub: https://github.com/Bytedesk/bytedesk
|
||||||
237
src/bytedesk-integration/App.jsx.example
Normal file
237
src/bytedesk-integration/App.jsx.example
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* vf_react App.jsx集成示例
|
||||||
|
*
|
||||||
|
* 本文件展示如何在vf_react项目中集成Bytedesk客服系统
|
||||||
|
*
|
||||||
|
* 集成步骤:
|
||||||
|
* 1. 将bytedesk-integration文件夹复制到src/目录
|
||||||
|
* 2. 在App.jsx中导入BytedeskWidget和配置
|
||||||
|
* 3. 添加BytedeskWidget组件(代码如下)
|
||||||
|
* 4. 配置.env文件(参考.env.bytedesk.example)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom'; // 如果使用react-router
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig, shouldShowCustomerService } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案一: 全局集成(推荐)
|
||||||
|
// 适用场景: 客服系统需要在所有页面显示
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// ========== vf_react原有代码保持不变 ==========
|
||||||
|
// 这里是您原有的App.jsx代码
|
||||||
|
// 例如: const [user, setUser] = useState(null);
|
||||||
|
// 例如: const [theme, setTheme] = useState('light');
|
||||||
|
// ... 保持原有逻辑不变 ...
|
||||||
|
|
||||||
|
// ========== Bytedesk集成代码开始 ==========
|
||||||
|
|
||||||
|
const location = useLocation(); // 获取当前路径
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
|
||||||
|
// 根据页面路径决定是否显示客服
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldShow = shouldShowCustomerService(location.pathname);
|
||||||
|
setShowBytedesk(shouldShow);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
// 获取Bytedesk配置
|
||||||
|
const bytedeskConfig = getBytedeskConfig();
|
||||||
|
|
||||||
|
// 客服加载成功回调
|
||||||
|
const handleBytedeskLoad = (bytedesk) => {
|
||||||
|
console.log('[App] Bytedesk客服系统加载成功', bytedesk);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 客服加载失败回调
|
||||||
|
const handleBytedeskError = (error) => {
|
||||||
|
console.error('[App] Bytedesk客服系统加载失败', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== Bytedesk集成代码结束 ==========
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
{/* ========== vf_react原有内容保持不变 ========== */}
|
||||||
|
{/* 这里是您原有的App.jsx JSX代码 */}
|
||||||
|
{/* 例如: <Header /> */}
|
||||||
|
{/* 例如: <Router> <Routes> ... </Routes> </Router> */}
|
||||||
|
{/* ... 保持原有结构不变 ... */}
|
||||||
|
|
||||||
|
{/* ========== Bytedesk客服Widget ========== */}
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
onLoad={handleBytedeskLoad}
|
||||||
|
onError={handleBytedeskError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案二: 带用户信息集成
|
||||||
|
// 适用场景: 需要将登录用户信息传递给客服端
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfigWithUser, shouldShowCustomerService } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
import { AuthContext } from './contexts/AuthContext'; // 假设您有用户认证Context
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// 获取登录用户信息
|
||||||
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldShow = shouldShowCustomerService(location.pathname);
|
||||||
|
setShowBytedesk(shouldShow);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
// 根据用户信息生成配置
|
||||||
|
const bytedeskConfig = user
|
||||||
|
? getBytedeskConfigWithUser(user)
|
||||||
|
: getBytedeskConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
// ... 您的原有代码 ...
|
||||||
|
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案三: 条件性加载
|
||||||
|
// 适用场景: 只在特定条件下显示客服(如用户已登录、特定用户角色等)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 只有在用户登录且为普通用户时显示客服
|
||||||
|
if (user && user.role === 'customer') {
|
||||||
|
setShowBytedesk(true);
|
||||||
|
} else {
|
||||||
|
setShowBytedesk(false);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const bytedeskConfig = getBytedeskConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
// ... 您的原有代码 ...
|
||||||
|
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案四: 动态控制显示/隐藏
|
||||||
|
// 适用场景: 需要通过按钮或其他交互控制客服显示
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
const bytedeskConfig = getBytedeskConfig();
|
||||||
|
|
||||||
|
const toggleBytedesk = () => {
|
||||||
|
setShowBytedesk(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
// ... 您的原有代码 ...
|
||||||
|
|
||||||
|
{/* 自定义客服按钮 *\/}
|
||||||
|
<button onClick={toggleBytedesk} className="custom-service-button">
|
||||||
|
{showBytedesk ? '关闭客服' : '联系客服'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 客服Widget *\/}
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 重要提示
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. CSS样式兼容性
|
||||||
|
* - Bytedesk Widget使用Shadow DOM,不会影响您的全局样式
|
||||||
|
* - Widget的样式可通过config中的theme配置调整
|
||||||
|
*
|
||||||
|
* 2. 性能优化
|
||||||
|
* - Widget脚本采用异步加载,不会阻塞页面渲染
|
||||||
|
* - 建议在非关键页面(如登录、支付页)隐藏客服
|
||||||
|
*
|
||||||
|
* 3. 错误处理
|
||||||
|
* - 如果客服脚本加载失败,不会影响主应用
|
||||||
|
* - 建议添加onError回调进行错误监控
|
||||||
|
*
|
||||||
|
* 4. 调试模式
|
||||||
|
* - 查看浏览器控制台的[Bytedesk]前缀日志
|
||||||
|
* - 检查Network面板确认脚本加载成功
|
||||||
|
*
|
||||||
|
* 5. 生产部署
|
||||||
|
* - 确保.env文件配置正确(特别是REACT_APP_BYTEDESK_API_URL)
|
||||||
|
* - 确保CORS已在后端配置(允许您的前端域名)
|
||||||
|
* - 在管理后台配置正确的工作组ID(sid)
|
||||||
|
*/
|
||||||
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;
|
||||||
130
src/bytedesk-integration/config/bytedesk.config.js
Normal file
130
src/bytedesk-integration/config/bytedesk.config.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Bytedesk客服配置文件
|
||||||
|
* 指向43.143.189.195服务器
|
||||||
|
*
|
||||||
|
* 环境变量配置(.env文件):
|
||||||
|
* REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
|
||||||
|
* REACT_APP_BYTEDESK_ORG=df_org_uid
|
||||||
|
* REACT_APP_BYTEDESK_SID=df_wg_aftersales
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 从环境变量读取配置
|
||||||
|
const BYTEDESK_API_URL = process.env.REACT_APP_BYTEDESK_API_URL || 'http://43.143.189.195';
|
||||||
|
const BYTEDESK_ORG = process.env.REACT_APP_BYTEDESK_ORG || 'df_org_uid';
|
||||||
|
const BYTEDESK_SID = process.env.REACT_APP_BYTEDESK_SID || 'df_wg_aftersales';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytedesk客服基础配置
|
||||||
|
*/
|
||||||
|
export const bytedeskConfig = {
|
||||||
|
// API服务地址
|
||||||
|
apiUrl: BYTEDESK_API_URL,
|
||||||
|
// 聊天页面地址
|
||||||
|
htmlUrl: `${BYTEDESK_API_URL}/chat/`,
|
||||||
|
|
||||||
|
// 客服图标位置
|
||||||
|
placement: 'bottom-right', // bottom-right | bottom-left | top-right | top-left
|
||||||
|
|
||||||
|
// 边距设置(像素)
|
||||||
|
marginBottom: 20,
|
||||||
|
marginSide: 20,
|
||||||
|
|
||||||
|
// 自动弹出(不推荐)
|
||||||
|
autoPopup: false,
|
||||||
|
|
||||||
|
// 语言设置
|
||||||
|
locale: 'zh-cn', // zh-cn | en | ja | ko
|
||||||
|
|
||||||
|
// 客服图标配置
|
||||||
|
bubbleConfig: {
|
||||||
|
show: true, // 是否显示客服图标
|
||||||
|
icon: '💬', // 图标(emoji或图片URL)
|
||||||
|
title: '在线客服', // 鼠标悬停标题
|
||||||
|
subtitle: '点击咨询', // 副标题
|
||||||
|
},
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
theme: {
|
||||||
|
mode: 'system', // light | dark | system
|
||||||
|
backgroundColor: '#0066FF', // 主题色
|
||||||
|
textColor: '#ffffff', // 文字颜色
|
||||||
|
},
|
||||||
|
|
||||||
|
// 聊天配置(必需)
|
||||||
|
chatConfig: {
|
||||||
|
org: BYTEDESK_ORG, // 组织ID
|
||||||
|
t: '2', // 类型: 2=客服, 1=机器人
|
||||||
|
sid: BYTEDESK_SID, // 工作组ID
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Bytedesk配置(根据环境自动切换)
|
||||||
|
*
|
||||||
|
* @returns {Object} Bytedesk配置对象
|
||||||
|
*/
|
||||||
|
export const getBytedeskConfig = () => {
|
||||||
|
// 开发环境使用代理(绕过X-Frame-Options限制)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return {
|
||||||
|
...bytedeskConfig,
|
||||||
|
apiUrl: '/bytedesk-api',
|
||||||
|
htmlUrl: '/bytedesk-api/chat/',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境使用完整URL
|
||||||
|
return bytedeskConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取带用户信息的配置
|
||||||
|
* 用于已登录用户,自动传递用户信息到客服端
|
||||||
|
*
|
||||||
|
* @param {Object} user - 用户对象
|
||||||
|
* @param {string} user.id - 用户ID
|
||||||
|
* @param {string} user.name - 用户名
|
||||||
|
* @param {string} user.email - 用户邮箱
|
||||||
|
* @param {string} user.mobile - 用户手机号
|
||||||
|
* @returns {Object} 带用户信息的Bytedesk配置
|
||||||
|
*/
|
||||||
|
export const getBytedeskConfigWithUser = (user) => {
|
||||||
|
const config = getBytedeskConfig();
|
||||||
|
|
||||||
|
if (user && user.id) {
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
chatConfig: {
|
||||||
|
...config.chatConfig,
|
||||||
|
// 传递用户信息(可选)
|
||||||
|
customParams: {
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.name || 'Guest',
|
||||||
|
userEmail: user.email || '',
|
||||||
|
userMobile: user.mobile || '',
|
||||||
|
source: 'web', // 来源标识
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据页面路径判断是否显示客服
|
||||||
|
*
|
||||||
|
* @param {string} pathname - 当前页面路径
|
||||||
|
* @returns {boolean} 是否显示客服
|
||||||
|
*/
|
||||||
|
export const shouldShowCustomerService = (pathname) => {
|
||||||
|
// 所有页面都显示Bytedesk客服
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
bytedeskConfig,
|
||||||
|
getBytedeskConfig,
|
||||||
|
getBytedeskConfigWithUser,
|
||||||
|
shouldShowCustomerService,
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// 集中管理应用的全局组件
|
// 集中管理应用的全局组件
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
@@ -12,6 +13,10 @@ import NotificationTestTool from './NotificationTestTool';
|
|||||||
import ConnectionStatusBar from './ConnectionStatusBar';
|
import ConnectionStatusBar from './ConnectionStatusBar';
|
||||||
import ScrollToTop from './ScrollToTop';
|
import ScrollToTop from './ScrollToTop';
|
||||||
|
|
||||||
|
// Bytedesk客服组件
|
||||||
|
import BytedeskWidget from '../bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig, shouldShowCustomerService } from '../bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConnectionStatusBar 包装组件
|
* ConnectionStatusBar 包装组件
|
||||||
* 需要在 NotificationProvider 内部使用,所以在这里包装
|
* 需要在 NotificationProvider 内部使用,所以在这里包装
|
||||||
@@ -67,8 +72,12 @@ function ConnectionStatusBarWrapper() {
|
|||||||
* - AuthModalManager: 认证弹窗管理器
|
* - AuthModalManager: 认证弹窗管理器
|
||||||
* - NotificationContainer: 通知容器
|
* - NotificationContainer: 通知容器
|
||||||
* - NotificationTestTool: 通知测试工具 (仅开发环境)
|
* - NotificationTestTool: 通知测试工具 (仅开发环境)
|
||||||
|
* - BytedeskWidget: Bytedesk在线客服 (条件性显示,在/和/home页隐藏)
|
||||||
*/
|
*/
|
||||||
export function GlobalComponents() {
|
export function GlobalComponents() {
|
||||||
|
const location = useLocation();
|
||||||
|
const showBytedesk = shouldShowCustomerService(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Socket 连接状态条 */}
|
{/* Socket 连接状态条 */}
|
||||||
@@ -85,6 +94,14 @@ export function GlobalComponents() {
|
|||||||
|
|
||||||
{/* 通知测试工具 (仅开发环境) */}
|
{/* 通知测试工具 (仅开发环境) */}
|
||||||
<NotificationTestTool />
|
<NotificationTestTool />
|
||||||
|
|
||||||
|
{/* Bytedesk在线客服 - 根据路径条件性显示 */}
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={getBytedeskConfig()}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user