Compare commits
26 Commits
feature_bu
...
feature_bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fe65f6c23 | ||
|
|
7fa4a8efbc | ||
|
|
44ae479615 | ||
|
|
e32a500247 | ||
|
|
5524826edd | ||
|
|
19b03b6c91 | ||
|
|
b07cb8ab51 | ||
|
|
a1c952c619 | ||
|
|
fb4a18c8ec | ||
|
|
1e9484e471 | ||
|
|
5c60450ba1 | ||
|
|
d2b6d891b2 | ||
|
|
261a7bf329 | ||
|
|
a3dfa5fd06 | ||
|
|
1b7bec47ee | ||
|
|
ccf1d1c0a6 | ||
|
|
e78f9a512f | ||
|
|
926ffa1b8f | ||
|
|
eebd207276 | ||
|
|
6b96744b2c | ||
|
|
463bdbf09c | ||
|
|
2bb8cb78e6 | ||
|
|
a15585c464 | ||
|
|
643c3db03e | ||
|
|
8e5623d723 | ||
|
|
57b4841b4c |
@@ -17,13 +17,13 @@ NODE_ENV=production
|
||||
REACT_APP_ENABLE_MOCK=false
|
||||
|
||||
# 🔧 调试模式(生产环境临时调试用)
|
||||
# 开启后会在全局暴露 window.__DEBUG__ 调试 API
|
||||
# 开启后会在全局暴露 window.__DEBUG__ 和 window.__TEST_NOTIFICATION__ 调试 API
|
||||
# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
|
||||
# 使用方法:
|
||||
# 1. 设置为 true 并重新构建
|
||||
# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
|
||||
# 3. 调试完成后设置为 false 并重新构建
|
||||
REACT_APP_ENABLE_DEBUG=false
|
||||
REACT_APP_ENABLE_DEBUG=true
|
||||
|
||||
# 后端 API 地址(生产环境)
|
||||
REACT_APP_API_URL=http://49.232.185.254:5001
|
||||
@@ -49,3 +49,20 @@ TSC_COMPILE_ON_ERROR=true
|
||||
IMAGE_INLINE_SIZE_LIMIT=10000
|
||||
# Node.js 内存限制(适用于大型项目)
|
||||
NODE_OPTIONS=--max_old_space_size=4096
|
||||
|
||||
# ========================================
|
||||
# Bytedesk 客服系统配置
|
||||
# ========================================
|
||||
# Bytedesk 服务器地址(使用相对路径,通过 Nginx 代理)
|
||||
# ⚠️ 重要:生产环境必须使用相对路径,避免 Mixed Content 错误
|
||||
# Nginx 配置:location /bytedesk-api/ { proxy_pass http://43.143.189.195/; }
|
||||
REACT_APP_BYTEDESK_API_URL=/bytedesk-api
|
||||
|
||||
# 组织 UUID(从管理后台 -> 设置 -> 组织信息 -> 组织UUID)
|
||||
REACT_APP_BYTEDESK_ORG=df_org_uid
|
||||
|
||||
# 工作组 UUID(从管理后台 -> 客服管理 -> 工作组 -> 工作组UUID)
|
||||
REACT_APP_BYTEDESK_SID=df_wg_uid
|
||||
|
||||
# 客服类型(2=人工客服, 1=机器人)
|
||||
REACT_APP_BYTEDESK_TYPE=2
|
||||
|
||||
49
BYTEDESK_INTEGRATION_FILES.txt
Normal file
49
BYTEDESK_INTEGRATION_FILES.txt
Normal file
@@ -0,0 +1,49 @@
|
||||
# Bytedesk 客服系统集成文件
|
||||
|
||||
以下文件和目录属于客服系统集成功能,未提交到当前分支:
|
||||
|
||||
## 1. Dify 机器人控制逻辑
|
||||
**位置**: public/index.html
|
||||
**状态**: 已存入 stash
|
||||
**Stash ID**: stash@{0}
|
||||
**说明**: 根据路径控制 Dify 机器人显示(已设置为完全不显示,只使用 Bytedesk 客服)
|
||||
|
||||
## 2. Bytedesk 集成代码
|
||||
**位置**: src/bytedesk-integration/
|
||||
**状态**: 未跟踪文件(需要手动管理)
|
||||
**内容**:
|
||||
- .env.bytedesk.example - Bytedesk 环境变量配置示例
|
||||
- App.jsx.example - 集成 Bytedesk 的示例代码
|
||||
- components/ - Bytedesk 相关组件
|
||||
- config/ - Bytedesk 配置文件
|
||||
- 前端工程师集成手册.md - 详细集成文档
|
||||
|
||||
## 恢复方法
|
||||
|
||||
### 恢复 public/index.html 的改动:
|
||||
```bash
|
||||
git stash apply stash@{0}
|
||||
```
|
||||
|
||||
### 使用 Bytedesk 集成代码:
|
||||
```bash
|
||||
# 查看集成手册
|
||||
cat src/bytedesk-integration/前端工程师集成手册.md
|
||||
|
||||
# 复制示例配置
|
||||
cp src/bytedesk-integration/.env.bytedesk.example .env.bytedesk
|
||||
cp src/bytedesk-integration/App.jsx.example src/App.jsx
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **重要提示:**
|
||||
- `src/bytedesk-integration/` 目录中的文件是未跟踪的(untracked)
|
||||
- 如果需要提交客服功能,需要先添加到 git:
|
||||
```bash
|
||||
git add src/bytedesk-integration/
|
||||
git commit -m "feat: 集成 Bytedesk 客服系统"
|
||||
```
|
||||
|
||||
- 当前分支(feature_bugfix/251110_event)专注于非客服功能
|
||||
- 建议在单独的分支中开发客服功能
|
||||
@@ -110,9 +110,6 @@ module.exports = {
|
||||
...webpackConfig.resolve,
|
||||
alias: {
|
||||
...webpackConfig.resolve.alias,
|
||||
// 强制 'debug' 模块解析到 node_modules(避免与 src/devtools/ 冲突)
|
||||
'debug': path.resolve(__dirname, 'node_modules/debug'),
|
||||
|
||||
// 根目录别名
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
|
||||
@@ -122,7 +119,6 @@ module.exports = {
|
||||
'@constants': path.resolve(__dirname, 'src/constants'),
|
||||
'@contexts': path.resolve(__dirname, 'src/contexts'),
|
||||
'@data': path.resolve(__dirname, 'src/data'),
|
||||
'@devtools': path.resolve(__dirname, 'src/devtools'),
|
||||
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
||||
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
||||
'@lib': path.resolve(__dirname, 'src/lib'),
|
||||
@@ -267,33 +263,13 @@ module.exports = {
|
||||
logLevel: 'debug',
|
||||
pathRewrite: { '^/concept-api': '' },
|
||||
},
|
||||
'/bytedesk-api': {
|
||||
target: 'http://43.143.189.195',
|
||||
'/bytedesk': {
|
||||
target: 'https://valuefrontier.cn', // 统一使用生产环境 Nginx 代理
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
secure: false, // 开发环境禁用 HTTPS 严格验证
|
||||
logLevel: 'debug',
|
||||
pathRewrite: { '^/bytedesk-api': '' },
|
||||
},
|
||||
'/chat': {
|
||||
target: 'http://43.143.189.195',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
// 不需要pathRewrite,保留/chat路径
|
||||
},
|
||||
'/config': {
|
||||
target: 'http://43.143.189.195',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
// 不需要pathRewrite,保留/config路径
|
||||
},
|
||||
'/visitor': {
|
||||
target: 'http://43.143.189.195',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
// 不需要pathRewrite,保留/visitor路径
|
||||
ws: true, // 支持 WebSocket
|
||||
// 不使用 pathRewrite,保留 /bytedesk 前缀,让生产 Nginx 处理
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
<!--
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Argon Dashboard Chakra PRO - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
|
||||
|
||||
* Designed and Coded by Simmmple & Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr" layout="admin">
|
||||
<head>
|
||||
@@ -25,10 +7,6 @@
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<link
|
||||
@@ -36,226 +14,11 @@
|
||||
sizes="76x76"
|
||||
href="%PUBLIC_URL%/apple-icon.png"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="./favicon.png" />
|
||||
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.png" or "favicon.png", "%PUBLIC_URL%/favicon.png" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>价值前沿——LLM赋能的分析平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script>
|
||||
window.difyChatbotConfig = {
|
||||
token: 'DwN8qAKtYFQtWskM',
|
||||
baseUrl: 'https://app.valuefrontier.cn',
|
||||
inputs: {
|
||||
// You can define the inputs from the Start node here
|
||||
// key is the variable name
|
||||
// e.g.
|
||||
// name: "NAME"
|
||||
},
|
||||
systemVariables: {
|
||||
// user_id: 'YOU CAN DEFINE USER ID HERE',
|
||||
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
|
||||
},
|
||||
userVariables: {
|
||||
// avatar_url: 'YOU CAN DEFINE USER AVATAR URL 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
|
||||
src="https://app.valuefrontier.cn/embed.min.js"
|
||||
id="DwN8qAKtYFQtWskM"
|
||||
defer>
|
||||
</script>
|
||||
<style>
|
||||
#dify-chatbot-bubble-button {
|
||||
background-color: #1C64F2 !important;
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
box-shadow: 0 4px 12px rgba(28, 100, 242, 0.3) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
#dify-chatbot-bubble-button:hover {
|
||||
transform: scale(1.1) !important;
|
||||
box-shadow: 0 6px 16px rgba(28, 100, 242, 0.4) !important;
|
||||
}
|
||||
|
||||
#dify-chatbot-bubble-window {
|
||||
width: 42rem !important;
|
||||
height: 80vh !important;
|
||||
max-height: calc(100vh - 2rem) !important;
|
||||
position: fixed !important;
|
||||
bottom: 100px !important;
|
||||
right: 20px !important;
|
||||
border-radius: 16px !important;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15) !important;
|
||||
border: 1px solid rgba(28, 100, 242, 0.1) !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* 确保Dify聊天窗口中的超链接正确显示 */
|
||||
#dify-chatbot-bubble-window a,
|
||||
#dify-chatbot-bubble-window a:link,
|
||||
#dify-chatbot-bubble-window a:visited,
|
||||
#dify-chatbot-bubble-window a:hover,
|
||||
#dify-chatbot-bubble-window a:active {
|
||||
color: #1C64F2 !important;
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* 确保超链接在Dify消息区域中可见 */
|
||||
#dify-chatbot-bubble-window .message-content a,
|
||||
#dify-chatbot-bubble-window .markdown-content a,
|
||||
#dify-chatbot-bubble-window [class*="message"] a {
|
||||
color: #0066cc !important;
|
||||
text-decoration: underline !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* 桌面端大屏优化 */
|
||||
@media (min-width: 1440px) {
|
||||
#dify-chatbot-bubble-window {
|
||||
width: 45rem !important;
|
||||
height: 85vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端适配 */
|
||||
@media (max-width: 1024px) and (min-width: 641px) {
|
||||
#dify-chatbot-bubble-window {
|
||||
width: 38rem !important;
|
||||
height: 75vh !important;
|
||||
right: 15px !important;
|
||||
bottom: 90px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 640px) {
|
||||
#dify-chatbot-bubble-window {
|
||||
width: calc(100vw - 20px) !important;
|
||||
height: 85vh !important;
|
||||
max-height: 85vh !important;
|
||||
right: 10px !important;
|
||||
bottom: 80px !important;
|
||||
left: 10px !important;
|
||||
}
|
||||
|
||||
#dify-chatbot-bubble-button {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -35,7 +35,7 @@ self.addEventListener('notificationclick', (event) => {
|
||||
.then((windowClients) => {
|
||||
// 查找是否已有打开的窗口
|
||||
for (let client of windowClients) {
|
||||
if (client.url.includes(window.location.origin) && 'focus' in client) {
|
||||
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
||||
// 聚焦现有窗口并导航到目标页面
|
||||
return client.focus().then(client => {
|
||||
return client.navigate(urlToOpen);
|
||||
|
||||
@@ -29,15 +29,16 @@ REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
|
||||
# Bytedesk组织和工作组配置(必需)
|
||||
# ============================================================================
|
||||
|
||||
# 组织ID(Organization UID)
|
||||
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 复制UID
|
||||
# 示例: df_org_uid
|
||||
# 组织 UUID(Organization UUID)
|
||||
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 组织UUID
|
||||
# 注意: 不是"组织代码",是"组织UUID"(df_org_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
|
||||
# 工作组 UUID(Workgroup UUID)
|
||||
# 获取方式: 登录管理后台 -> 客服管理 -> 工作组 -> 工作组UUID
|
||||
# 当前配置: df_wg_uid(默认工作组)
|
||||
REACT_APP_BYTEDESK_SID=df_wg_uid
|
||||
|
||||
# ============================================================================
|
||||
# 可选配置
|
||||
|
||||
@@ -82,65 +82,28 @@ const BytedeskWidget = ({
|
||||
return () => {
|
||||
console.log('[Bytedesk] 清理Widget');
|
||||
|
||||
try {
|
||||
// 调用Widget的destroy方法(如果存在)
|
||||
if (widgetRef.current && typeof widgetRef.current.destroy === 'function') {
|
||||
console.log('[Bytedesk] 调用Widget.destroy()');
|
||||
widgetRef.current.destroy();
|
||||
// 移除脚本
|
||||
if (scriptRef.current && document.body.contains(scriptRef.current)) {
|
||||
document.body.removeChild(scriptRef.current);
|
||||
}
|
||||
|
||||
// 移除Widget DOM元素
|
||||
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
|
||||
widgetElements.forEach(el => {
|
||||
if (el && el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
} 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);
|
||||
// 清理全局对象
|
||||
if (window.BytedeskWeb) {
|
||||
delete window.BytedeskWeb;
|
||||
}
|
||||
|
||||
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;
|
||||
// 不渲染任何可见元素(Widget会自动插入到body)
|
||||
return <div id="bytedesk-widget-container" style={{ display: 'none' }} />;
|
||||
};
|
||||
|
||||
BytedeskWidget.propTypes = {
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
/**
|
||||
* Bytedesk客服配置文件
|
||||
* 指向43.143.189.195服务器
|
||||
* 通过代理访问 Bytedesk 服务器(解决 HTTPS 混合内容问题)
|
||||
*
|
||||
* 环境变量配置(.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
|
||||
* REACT_APP_BYTEDESK_SID=df_wg_uid
|
||||
*
|
||||
* 架构说明:
|
||||
* - iframe 使用完整域名:https://valuefrontier.cn/bytedesk/chat/
|
||||
* - 使用 HTTPS 协议,解决生产环境 Mixed Content 错误
|
||||
* - 本地:CRACO 代理 /bytedesk → valuefrontier.cn/bytedesk
|
||||
* - 生产:前端 Nginx 代理 /bytedesk → 43.143.189.195
|
||||
* - baseUrl 保持官方 CDN(用于加载 SDK 外部模块)
|
||||
*
|
||||
* ⚠️ 注意:需要前端 Nginx 配置 /bytedesk/ 代理规则
|
||||
*/
|
||||
|
||||
// 从环境变量读取配置
|
||||
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';
|
||||
const BYTEDESK_SID = process.env.REACT_APP_BYTEDESK_SID || 'df_wg_uid';
|
||||
|
||||
/**
|
||||
* Bytedesk客服基础配置
|
||||
*/
|
||||
export const bytedeskConfig = {
|
||||
// API服务地址
|
||||
apiUrl: BYTEDESK_API_URL,
|
||||
// 聊天页面地址
|
||||
htmlUrl: `${BYTEDESK_API_URL}/chat/`,
|
||||
// API服务地址(如果 SDK 需要调用 API)
|
||||
apiUrl: '/bytedesk/',
|
||||
// 聊天页面地址(使用完整 HTTPS 域名,通过 /bytedesk/ 代理避免 React Router 冲突)
|
||||
htmlUrl: 'https://valuefrontier.cn/chat/',
|
||||
// SDK 资源基础路径(保持 Bytedesk 官方 CDN,用于加载外部模块)
|
||||
baseUrl: 'https://www.weiyuai.cn',
|
||||
|
||||
// 客服图标位置
|
||||
placement: 'bottom-right', // bottom-right | bottom-left | top-right | top-left
|
||||
@@ -53,7 +62,7 @@ export const bytedeskConfig = {
|
||||
// 聊天配置(必需)
|
||||
chatConfig: {
|
||||
org: BYTEDESK_ORG, // 组织ID
|
||||
t: '2', // 类型: 2=客服, 1=机器人
|
||||
t: '1', // 类型: 1=人工客服, 2=机器人
|
||||
sid: BYTEDESK_SID, // 工作组ID
|
||||
},
|
||||
};
|
||||
@@ -64,16 +73,7 @@ export const bytedeskConfig = {
|
||||
* @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;
|
||||
};
|
||||
|
||||
@@ -118,8 +118,33 @@ export const getBytedeskConfigWithUser = (user) => {
|
||||
* @returns {boolean} 是否显示客服
|
||||
*/
|
||||
export const shouldShowCustomerService = (pathname) => {
|
||||
// 所有页面都显示Bytedesk客服
|
||||
// 在以下页面隐藏客服(黑名单)
|
||||
const blockedPages = [
|
||||
// '/home', // 登录页
|
||||
];
|
||||
|
||||
// 检查是否在黑名单
|
||||
if (blockedPages.some(page => pathname.startsWith(page))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 默认所有页面都显示客服
|
||||
return true;
|
||||
|
||||
/* ============================================
|
||||
白名单模式(备用,需要时取消注释)
|
||||
============================================
|
||||
const allowedPages = [
|
||||
'/', // 首页
|
||||
'/home', // 主页
|
||||
'/products', // 产品页
|
||||
'/pricing', // 价格页
|
||||
'/contact', // 联系我们
|
||||
];
|
||||
|
||||
// 只在白名单页面显示客服
|
||||
return allowedPages.some(page => pathname.startsWith(page));
|
||||
============================================ */
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -59,6 +59,11 @@ export const NotificationProvider = ({ children }) => {
|
||||
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
||||
const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
|
||||
|
||||
// ⚡ 方案2: 使用 Ref 存储最新的回调函数引用(避免闭包陷阱)
|
||||
const addNotificationRef = useRef(null);
|
||||
const adaptEventToNotificationRef = useRef(null);
|
||||
const isFirstConnect = useRef(true); // 标记是否首次连接
|
||||
|
||||
// ⚡ 使用权限引导管理 Hook
|
||||
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
||||
|
||||
@@ -595,26 +600,42 @@ export const NotificationProvider = ({ children }) => {
|
||||
return newNotification.id;
|
||||
}, [notifications, toast, sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]);
|
||||
|
||||
// 连接到 Socket 服务
|
||||
/**
|
||||
* ✅ 方案2: 同步最新的回调函数到 Ref
|
||||
* 确保 Socket 监听器始终使用最新的函数引用(避免闭包陷阱)
|
||||
*/
|
||||
useEffect(() => {
|
||||
addNotificationRef.current = addNotification;
|
||||
console.log('[NotificationContext] 📝 已更新 addNotificationRef');
|
||||
}, [addNotification]);
|
||||
|
||||
useEffect(() => {
|
||||
adaptEventToNotificationRef.current = adaptEventToNotification;
|
||||
console.log('[NotificationContext] 📝 已更新 adaptEventToNotificationRef');
|
||||
}, [adaptEventToNotification]);
|
||||
|
||||
|
||||
// ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ==========
|
||||
useEffect(() => {
|
||||
logger.info('NotificationContext', 'Initializing socket connection...');
|
||||
console.log('%c[NotificationContext] Initializing socket connection', 'color: #673AB7; font-weight: bold;');
|
||||
console.log('%c[NotificationContext] 🚀 初始化 Socket 连接(方案2:只注册一次)', 'color: #673AB7; font-weight: bold;');
|
||||
|
||||
// ✅ 第一步: 注册所有事件监听器
|
||||
console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;');
|
||||
|
||||
// 监听连接状态
|
||||
// ========== 监听连接成功(首次连接 + 重连) ==========
|
||||
socket.on('connect', () => {
|
||||
const wasDisconnected = connectionStatus !== CONNECTION_STATUS.CONNECTED;
|
||||
setIsConnected(true);
|
||||
setReconnectAttempt(0);
|
||||
logger.info('NotificationContext', 'Socket connected', { wasDisconnected });
|
||||
console.log('%c[NotificationContext] ✅ Received connect event, updating state to connected', 'color: #4CAF50; font-weight: bold;');
|
||||
|
||||
// 如果之前断开过,显示 RECONNECTED 状态2秒后自动消失
|
||||
if (wasDisconnected) {
|
||||
// 判断是首次连接还是重连
|
||||
if (isFirstConnect.current) {
|
||||
console.log('%c[NotificationContext] ✅ 首次连接成功', 'color: #4CAF50; font-weight: bold;');
|
||||
console.log('[NotificationContext] Socket ID:', socket.getSocketId?.());
|
||||
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
||||
isFirstConnect.current = false;
|
||||
logger.info('NotificationContext', 'Socket connected (first time)');
|
||||
} else {
|
||||
console.log('%c[NotificationContext] 🔄 重连成功!', 'color: #FF9800; font-weight: bold;');
|
||||
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
|
||||
logger.info('NotificationContext', 'Reconnected, will auto-dismiss in 2s');
|
||||
logger.info('NotificationContext', 'Socket reconnected');
|
||||
|
||||
// 清除之前的定时器
|
||||
if (reconnectedTimerRef.current) {
|
||||
@@ -626,12 +647,10 @@ export const NotificationProvider = ({ children }) => {
|
||||
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
||||
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
|
||||
}, 2000);
|
||||
} else {
|
||||
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
||||
}
|
||||
|
||||
// 订阅事件推送
|
||||
console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
|
||||
// ⚡ 重连后只需重新订阅,不需要重新注册监听器
|
||||
console.log('%c[NotificationContext] 🔔 重新订阅事件推送...', 'color: #FF9800; font-weight: bold;');
|
||||
|
||||
if (socket.subscribeToEvents) {
|
||||
socket.subscribeToEvents({
|
||||
@@ -642,45 +661,47 @@ export const NotificationProvider = ({ children }) => {
|
||||
console.log('[NotificationContext] 订阅确认:', data);
|
||||
logger.info('NotificationContext', 'Events subscribed', data);
|
||||
},
|
||||
// ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
|
||||
});
|
||||
} else {
|
||||
console.error('[NotificationContext] ❌ socket.subscribeToEvents 方法不可用');
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 监听断开连接 ==========
|
||||
socket.on('disconnect', (reason) => {
|
||||
setIsConnected(false);
|
||||
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
|
||||
logger.warn('NotificationContext', 'Socket disconnected', { reason });
|
||||
console.log('%c[NotificationContext] ⚠️ Socket 已断开', 'color: #FF5722;', { reason });
|
||||
});
|
||||
|
||||
// 监听连接错误
|
||||
// ========== 监听连接错误 ==========
|
||||
socket.on('connect_error', (error) => {
|
||||
logger.error('NotificationContext', 'Socket connect_error', error);
|
||||
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
||||
|
||||
// 获取重连次数
|
||||
const attempts = socket.getReconnectAttempts?.() || 0;
|
||||
setReconnectAttempt(attempts);
|
||||
logger.info('NotificationContext', 'Reconnection attempt', { attempts });
|
||||
console.log(`%c[NotificationContext] 🔄 重连中... (第 ${attempts} 次尝试)`, 'color: #FF9800;');
|
||||
});
|
||||
|
||||
// 监听重连失败
|
||||
// ========== 监听重连失败 ==========
|
||||
socket.on('reconnect_failed', () => {
|
||||
logger.error('NotificationContext', 'Socket reconnect_failed');
|
||||
setConnectionStatus(CONNECTION_STATUS.FAILED);
|
||||
console.error('[NotificationContext] ❌ 重连失败');
|
||||
|
||||
toast({
|
||||
title: '连接失败',
|
||||
description: '无法连接到服务器,请检查网络连接',
|
||||
status: 'error',
|
||||
duration: null, // 不自动关闭
|
||||
duration: null,
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
|
||||
// 监听新事件推送(统一事件名)
|
||||
// ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ==========
|
||||
socket.on('new_event', (data) => {
|
||||
console.log('\n%c════════════════════════════════════════', 'color: #FF9800; font-weight: bold;');
|
||||
console.log('%c[NotificationContext] 📨 收到 new_event 事件!', 'color: #FF9800; font-weight: bold;');
|
||||
@@ -693,17 +714,24 @@ export const NotificationProvider = ({ children }) => {
|
||||
|
||||
logger.info('NotificationContext', 'Received new event', data);
|
||||
|
||||
// ========== Socket层去重检查 ==========
|
||||
// 生成更健壮的事件ID
|
||||
const eventId = data.id ||
|
||||
`${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
// ⚠️ 防御性检查:确保 ref 已初始化
|
||||
if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
|
||||
console.error('%c[NotificationContext] ❌ Ref 未初始化,跳过处理', 'color: #F44336; font-weight: bold;');
|
||||
logger.error('NotificationContext', 'Refs not initialized', {
|
||||
addNotificationRef: !!addNotificationRef.current,
|
||||
adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ========== Socket层去重检查 ==========
|
||||
const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// 如果缺少原始ID,记录警告
|
||||
if (!data.id) {
|
||||
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
|
||||
eventId,
|
||||
eventType: data.type,
|
||||
title: data.title
|
||||
title: data.title,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -711,55 +739,61 @@ export const NotificationProvider = ({ children }) => {
|
||||
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
||||
console.warn('[NotificationContext] ⚠️ 重复事件,已忽略:', eventId);
|
||||
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
|
||||
return; // 重复事件,直接忽略
|
||||
return;
|
||||
}
|
||||
|
||||
// 记录已处理的事件ID
|
||||
processedEventIds.current.add(eventId);
|
||||
console.log('[NotificationContext] ✓ 事件已记录,防止重复处理');
|
||||
|
||||
// 限制Set大小,避免内存泄漏
|
||||
// 限制 Set 大小,避免内存泄漏
|
||||
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
|
||||
const idsArray = Array.from(processedEventIds.current);
|
||||
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
|
||||
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
|
||||
kept: MAX_PROCESSED_IDS
|
||||
kept: MAX_PROCESSED_IDS,
|
||||
});
|
||||
}
|
||||
// ========== Socket层去重检查结束 ==========
|
||||
|
||||
// 使用适配器转换事件格式
|
||||
// ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
|
||||
console.log('[NotificationContext] 正在转换事件格式...');
|
||||
const notification = adaptEventToNotification(data);
|
||||
const notification = adaptEventToNotificationRef.current(data);
|
||||
console.log('[NotificationContext] 转换后的通知对象:', notification);
|
||||
|
||||
// ✅ 使用 ref.current 访问最新的 addNotification 函数
|
||||
console.log('[NotificationContext] 准备添加通知到队列...');
|
||||
addNotification(notification);
|
||||
addNotificationRef.current(notification);
|
||||
console.log('[NotificationContext] ✅ 通知已添加到队列');
|
||||
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
|
||||
});
|
||||
|
||||
// 保留系统通知监听(兼容性)
|
||||
// ========== 监听系统通知(兼容性) ==========
|
||||
socket.on('system_notification', (data) => {
|
||||
logger.info('NotificationContext', 'Received system notification', data);
|
||||
addNotification(data);
|
||||
console.log('[NotificationContext] 📢 收到系统通知:', data);
|
||||
|
||||
if (addNotificationRef.current) {
|
||||
addNotificationRef.current(data);
|
||||
} else {
|
||||
console.error('[NotificationContext] ❌ addNotificationRef 未初始化');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('%c[NotificationContext] ✅ All event listeners registered', 'color: #4CAF50; font-weight: bold;');
|
||||
console.log('%c[NotificationContext] ✅ 所有监听器已注册(只注册一次)', 'color: #4CAF50; font-weight: bold;');
|
||||
|
||||
// ✅ 第二步: 获取最大重连次数
|
||||
// ========== 获取最大重连次数 ==========
|
||||
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
|
||||
setMaxReconnectAttempts(maxAttempts);
|
||||
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
|
||||
|
||||
// ✅ 第三步: 调用 socket.connect()
|
||||
console.log('%c[NotificationContext] Step 2: Calling socket.connect()...', 'color: #673AB7; font-weight: bold;');
|
||||
// ========== 启动连接 ==========
|
||||
console.log('%c[NotificationContext] 🔌 调用 socket.connect()...', 'color: #673AB7; font-weight: bold;');
|
||||
socket.connect();
|
||||
console.log('%c[NotificationContext] socket.connect() completed', 'color: #673AB7;');
|
||||
|
||||
// 清理函数
|
||||
// ========== 清理函数(组件卸载时) ==========
|
||||
return () => {
|
||||
logger.info('NotificationContext', 'Cleaning up socket connection');
|
||||
console.log('%c[NotificationContext] 🧹 清理 Socket 连接', 'color: #9E9E9E;');
|
||||
|
||||
// 清理 reconnected 状态定时器
|
||||
if (reconnectedTimerRef.current) {
|
||||
@@ -774,15 +808,20 @@ export const NotificationProvider = ({ children }) => {
|
||||
});
|
||||
notificationTimers.current.clear();
|
||||
|
||||
// 移除所有事件监听器
|
||||
socket.off('connect');
|
||||
socket.off('disconnect');
|
||||
socket.off('connect_error');
|
||||
socket.off('reconnect_failed');
|
||||
socket.off('new_event');
|
||||
socket.off('system_notification');
|
||||
|
||||
// 断开连接
|
||||
socket.disconnect();
|
||||
|
||||
console.log('%c[NotificationContext] ✅ 清理完成', 'color: #4CAF50;');
|
||||
};
|
||||
}, []); // ✅ 空依赖数组,确保只执行一次,避免 React 严格模式重复执行
|
||||
}, []); // ⚠️ 空依赖数组,确保只执行一次
|
||||
|
||||
// ==================== 智能自动重试 ====================
|
||||
|
||||
@@ -896,6 +935,92 @@ export const NotificationProvider = ({ children }) => {
|
||||
};
|
||||
}, [browserPermission, toast]);
|
||||
|
||||
// 🔧 开发环境调试:暴露方法到 window
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_DEBUG === 'true') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__TEST_NOTIFICATION__ = {
|
||||
// 手动触发网页通知
|
||||
testWebNotification: (type = 'event_alert', priority = 'normal') => {
|
||||
console.log('%c[Debug] 手动触发网页通知', 'color: #FF9800; font-weight: bold;');
|
||||
|
||||
const testData = {
|
||||
id: `test_${Date.now()}`,
|
||||
type: type,
|
||||
priority: priority,
|
||||
title: '🧪 测试网页通知',
|
||||
content: `这是一条测试${type === 'announcement' ? '公告' : type === 'stock_alert' ? '股票' : type === 'event_alert' ? '事件' : '分析'}通知 (优先级: ${priority})`,
|
||||
timestamp: Date.now(),
|
||||
clickable: true,
|
||||
link: '/home',
|
||||
};
|
||||
|
||||
console.log('测试数据:', testData);
|
||||
addNotification(testData);
|
||||
console.log('✅ 通知已添加到队列');
|
||||
},
|
||||
|
||||
// 测试所有类型
|
||||
testAllTypes: () => {
|
||||
console.log('%c[Debug] 测试所有通知类型', 'color: #FF9800; font-weight: bold;');
|
||||
const types = ['announcement', 'stock_alert', 'event_alert', 'analysis_report'];
|
||||
types.forEach((type, i) => {
|
||||
setTimeout(() => {
|
||||
window.__TEST_NOTIFICATION__.testWebNotification(type, 'normal');
|
||||
}, i * 2000); // 每 2 秒一个
|
||||
});
|
||||
},
|
||||
|
||||
// 测试所有优先级
|
||||
testAllPriorities: () => {
|
||||
console.log('%c[Debug] 测试所有优先级', 'color: #FF9800; font-weight: bold;');
|
||||
const priorities = ['normal', 'important', 'urgent'];
|
||||
priorities.forEach((priority, i) => {
|
||||
setTimeout(() => {
|
||||
window.__TEST_NOTIFICATION__.testWebNotification('event_alert', priority);
|
||||
}, i * 2000);
|
||||
});
|
||||
},
|
||||
|
||||
// 帮助
|
||||
help: () => {
|
||||
console.log('\n%c=== 网页通知测试 API ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||
console.log('\n%c基础用法:', 'color: #2196F3; font-weight: bold;');
|
||||
console.log(' window.__TEST_NOTIFICATION__.testWebNotification(type, priority)');
|
||||
console.log('\n%c参数说明:', 'color: #2196F3; font-weight: bold;');
|
||||
console.log(' type (通知类型):');
|
||||
console.log(' - "announcement" 公告通知(蓝色)');
|
||||
console.log(' - "stock_alert" 股票动向(红色/绿色)');
|
||||
console.log(' - "event_alert" 事件动向(橙色)');
|
||||
console.log(' - "analysis_report" 分析报告(紫色)');
|
||||
console.log('\n priority (优先级):');
|
||||
console.log(' - "normal" 普通(15秒自动关闭)');
|
||||
console.log(' - "important" 重要(30秒自动关闭)');
|
||||
console.log(' - "urgent" 紧急(不自动关闭)');
|
||||
console.log('\n%c示例:', 'color: #4CAF50; font-weight: bold;');
|
||||
console.log(' // 测试紧急事件通知');
|
||||
console.log(' window.__TEST_NOTIFICATION__.testWebNotification("event_alert", "urgent")');
|
||||
console.log('\n // 测试所有类型');
|
||||
console.log(' window.__TEST_NOTIFICATION__.testAllTypes()');
|
||||
console.log('\n // 测试所有优先级');
|
||||
console.log(' window.__TEST_NOTIFICATION__.testAllPriorities()');
|
||||
console.log('\n');
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[NotificationContext] 🔧 调试 API 已加载: window.__TEST_NOTIFICATION__');
|
||||
console.log('[NotificationContext] 💡 使用 window.__TEST_NOTIFICATION__.help() 查看帮助');
|
||||
}
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
|
||||
delete window.__TEST_NOTIFICATION__;
|
||||
}
|
||||
};
|
||||
}, [addNotification]); // 依赖 addNotification 函数
|
||||
|
||||
const value = {
|
||||
notifications,
|
||||
isConnected,
|
||||
|
||||
@@ -71,7 +71,10 @@ class DebugToolkit {
|
||||
console.log('');
|
||||
console.log('%c2️⃣ 通知调试:', 'color: #9C27B0; font-weight: bold;');
|
||||
console.log(' __DEBUG__.notification.getLogs() - 获取所有通知日志');
|
||||
console.log(' __DEBUG__.notification.forceNotification() - 发送测试通知');
|
||||
console.log(' __DEBUG__.notification.forceNotification() - 发送测试浏览器通知');
|
||||
console.log(' __DEBUG__.notification.testWebNotification(type, priority) - 测试网页通知 🆕');
|
||||
console.log(' __DEBUG__.notification.testAllNotificationTypes() - 测试所有类型 🆕');
|
||||
console.log(' __DEBUG__.notification.testAllNotificationPriorities() - 测试所有优先级 🆕');
|
||||
console.log(' __DEBUG__.notification.checkPermission() - 检查通知权限');
|
||||
console.log(' __DEBUG__.notification.exportLogs() - 导出通知日志');
|
||||
console.log('');
|
||||
|
||||
@@ -159,6 +159,44 @@ class NotificationDebugger {
|
||||
getRecentEvents(count = 10) {
|
||||
return this.eventLog.slice(0, count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试网页通知(需要 window.__TEST_NOTIFICATION__ 可用)
|
||||
*/
|
||||
testWebNotification(type = 'event_alert', priority = 'normal') {
|
||||
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
|
||||
console.log('[Notification Debugger] 调用测试 API');
|
||||
window.__TEST_NOTIFICATION__.testWebNotification(type, priority);
|
||||
} else {
|
||||
console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
|
||||
console.error('💡 请确保:');
|
||||
console.error(' 1. REACT_APP_ENABLE_DEBUG=true');
|
||||
console.error(' 2. NotificationContext 已加载');
|
||||
console.error(' 3. 页面已刷新');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试所有通知类型
|
||||
*/
|
||||
testAllNotificationTypes() {
|
||||
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
|
||||
window.__TEST_NOTIFICATION__.testAllTypes();
|
||||
} else {
|
||||
console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试所有优先级
|
||||
*/
|
||||
testAllNotificationPriorities() {
|
||||
if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
|
||||
window.__TEST_NOTIFICATION__.testAllPriorities();
|
||||
} else {
|
||||
console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
|
||||
53
src/index.js
53
src/index.js
@@ -6,6 +6,9 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import './styles/brainwave.css';
|
||||
import './styles/brainwave-colors.css';
|
||||
|
||||
// 导入 Bytedesk 客服系统 z-index 覆盖样式(必须在所有样式之后导入)
|
||||
import './styles/bytedesk-override.css';
|
||||
|
||||
// Import the main App component
|
||||
import App from './App';
|
||||
|
||||
@@ -44,24 +47,64 @@ function registerServiceWorker() {
|
||||
navigator.serviceWorker
|
||||
.register('/service-worker.js')
|
||||
.then((registration) => {
|
||||
console.log('[App] Service Worker registered successfully:', registration.scope);
|
||||
console.log('[App] ✅ Service Worker 注册成功');
|
||||
console.log('[App] Scope:', registration.scope);
|
||||
|
||||
// 监听更新
|
||||
// 检查当前激活状态
|
||||
if (navigator.serviceWorker.controller) {
|
||||
console.log('[App] ✅ Service Worker 已激活并控制页面');
|
||||
} else {
|
||||
console.log('[App] ⏳ Service Worker 已注册,等待激活...');
|
||||
console.log('[App] 💡 刷新页面以激活 Service Worker');
|
||||
|
||||
// 监听 controller 变化(Service Worker 激活后触发)
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.log('[App] ✅ Service Worker 控制器已更新');
|
||||
});
|
||||
}
|
||||
|
||||
// 监听 Service Worker 更新
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
console.log('[App] Service Worker update found');
|
||||
console.log('[App] 🔄 发现 Service Worker 更新');
|
||||
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
console.log(`[App] Service Worker 状态: ${newWorker.state}`);
|
||||
if (newWorker.state === 'activated') {
|
||||
console.log('[App] Service Worker activated');
|
||||
console.log('[App] ✅ Service Worker 已激活');
|
||||
|
||||
// 如果有旧的 Service Worker 在控制页面,提示用户刷新
|
||||
if (navigator.serviceWorker.controller) {
|
||||
console.log('[App] 💡 Service Worker 已更新,建议刷新页面');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[App] Service Worker registration failed:', error);
|
||||
console.error('[App] ❌ Service Worker 注册失败');
|
||||
console.error('[App] 错误类型:', error.name);
|
||||
console.error('[App] 错误信息:', error.message);
|
||||
console.error('[App] 完整错误:', error);
|
||||
|
||||
// 额外检查:验证文件是否可访问
|
||||
fetch('/service-worker.js', { method: 'HEAD' })
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
console.error('[App] Service Worker 文件存在但注册失败');
|
||||
console.error('[App] 💡 可能的原因:');
|
||||
console.error('[App] 1. Service Worker 文件有语法错误');
|
||||
console.error('[App] 2. 浏览器不支持某些 Service Worker 特性');
|
||||
console.error('[App] 3. HTTPS 证书问题(Service Worker 需要 HTTPS)');
|
||||
} else {
|
||||
console.error('[App] Service Worker 文件不存在(HTTP', response.status, ')');
|
||||
}
|
||||
})
|
||||
.catch(fetchError => {
|
||||
console.error('[App] 无法访问 Service Worker 文件:', fetchError.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -217,7 +217,7 @@ export const REVENUE_EVENTS = {
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SPECIAL EVENTS (特殊事件) - Errors, performance, chatbot
|
||||
// SPECIAL EVENTS (特殊事件) - Errors, performance
|
||||
// ============================================================================
|
||||
export const SPECIAL_EVENTS = {
|
||||
// Errors
|
||||
@@ -229,13 +229,6 @@ export const SPECIAL_EVENTS = {
|
||||
PAGE_LOAD_TIME: 'Page Load Time',
|
||||
API_RESPONSE_TIME: 'API Response Time',
|
||||
|
||||
// Chatbot (Dify)
|
||||
CHATBOT_OPENED: 'Chatbot Opened',
|
||||
CHATBOT_CLOSED: 'Chatbot Closed',
|
||||
CHATBOT_MESSAGE_SENT: 'Chatbot Message Sent',
|
||||
CHATBOT_MESSAGE_RECEIVED: 'Chatbot Message Received',
|
||||
CHATBOT_FEEDBACK_PROVIDED: 'Chatbot Feedback Provided',
|
||||
|
||||
// Scroll depth
|
||||
SCROLL_DEPTH_25: 'Scroll Depth 25%',
|
||||
SCROLL_DEPTH_50: 'Scroll Depth 50%',
|
||||
|
||||
@@ -10,6 +10,21 @@ import { socketService } from '../socketService';
|
||||
export const socket = socketService;
|
||||
export { socketService };
|
||||
|
||||
// ⚡ 新增:暴露 Socket 实例到 window(用于调试和验证)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.socket = socketService;
|
||||
window.socketService = socketService;
|
||||
|
||||
console.log(
|
||||
'%c[Socket Service] ✅ Socket instance exposed to window',
|
||||
'color: #4CAF50; font-weight: bold; font-size: 14px;'
|
||||
);
|
||||
console.log(' 📍 window.socket:', window.socket);
|
||||
console.log(' 📍 window.socketService:', window.socketService);
|
||||
console.log(' 📍 Socket.IO instance:', window.socket?.socket);
|
||||
console.log(' 📍 Connection status:', window.socket?.connected ? '✅ Connected' : '❌ Disconnected');
|
||||
}
|
||||
|
||||
// 打印当前使用的服务类型
|
||||
console.log(
|
||||
'%c[Socket Service] Using REAL Socket Service',
|
||||
|
||||
@@ -51,13 +51,21 @@ class SocketService {
|
||||
...options,
|
||||
});
|
||||
|
||||
// 注册所有暂存的事件监听器
|
||||
// 注册所有暂存的事件监听器(保留 pendingListeners,不清空)
|
||||
if (this.pendingListeners.length > 0) {
|
||||
console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
|
||||
this.pendingListeners.forEach(({ event, callback }) => {
|
||||
this.on(event, callback);
|
||||
// 直接在 Socket.IO 实例上注册(避免递归调用 this.on())
|
||||
const wrappedCallback = (...args) => {
|
||||
console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;');
|
||||
console.log(`[socketService] 事件数据 (${event}):`, ...args);
|
||||
callback(...args);
|
||||
};
|
||||
|
||||
this.socket.on(event, wrappedCallback);
|
||||
console.log(`[socketService] ✓ 已注册事件监听器: ${event}`);
|
||||
});
|
||||
this.pendingListeners = []; // 清空暂存队列
|
||||
// ⚠️ 重要:不清空 pendingListeners,保留用于重连
|
||||
}
|
||||
|
||||
// 监听连接成功
|
||||
@@ -157,10 +165,18 @@ class SocketService {
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (!this.socket) {
|
||||
// Socket 未初始化,暂存监听器
|
||||
logger.info('socketService', 'Socket not ready, queuing listener', { event });
|
||||
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
|
||||
this.pendingListeners.push({ event, callback });
|
||||
// Socket 未初始化,暂存监听器(检查是否已存在,避免重复)
|
||||
const exists = this.pendingListeners.some(
|
||||
(listener) => listener.event === event && listener.callback === callback
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
logger.info('socketService', 'Socket not ready, queuing listener', { event });
|
||||
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
|
||||
this.pendingListeners.push({ event, callback });
|
||||
} else {
|
||||
console.log(`[socketService] ⚠️ 监听器已存在,跳过: ${event}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
35
src/styles/bytedesk-override.css
Normal file
35
src/styles/bytedesk-override.css
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Bytedesk 客服系统 z-index 覆盖样式
|
||||
*
|
||||
* 问题: Bytedesk 默认 z-index 为 10001,项目中部分元素使用 z-index: 99999
|
||||
* 导致客服 iframe 在首页被内容区覆盖,不可见
|
||||
*
|
||||
* 解决方案: 将所有 Bytedesk 相关元素的 z-index 提升到 999999
|
||||
* 确保客服窗口始终显示在最上层
|
||||
*/
|
||||
|
||||
/* Bytedesk 主容器 - 客服图标按钮 */
|
||||
[class*="bytedesk"],
|
||||
[id*="bytedesk"],
|
||||
[class*="BytedeskWeb"] {
|
||||
z-index: 999999 !important;
|
||||
}
|
||||
|
||||
/* Bytedesk iframe - 聊天窗口 */
|
||||
iframe[src*="bytedesk"],
|
||||
iframe[src*="/chat/"],
|
||||
iframe[src*="/visitor/"] {
|
||||
z-index: 999999 !important;
|
||||
}
|
||||
|
||||
/* Bytedesk 覆盖层(如果存在) */
|
||||
.bytedesk-overlay,
|
||||
[class*="bytedesk-overlay"] {
|
||||
z-index: 999998 !important;
|
||||
}
|
||||
|
||||
/* Bytedesk 通知气泡 */
|
||||
.bytedesk-badge,
|
||||
[class*="bytedesk-badge"] {
|
||||
z-index: 1000000 !important;
|
||||
}
|
||||
@@ -44,6 +44,8 @@ const Community = () => {
|
||||
|
||||
// Chakra UI hooks
|
||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||
const alertBgColor = useColorModeValue('blue.50', 'blue.900');
|
||||
const alertBorderColor = useColorModeValue('blue.200', 'blue.700');
|
||||
|
||||
// Ref:用于首次滚动到内容区域
|
||||
const containerRef = useRef(null);
|
||||
@@ -145,9 +147,9 @@ const Community = () => {
|
||||
borderRadius="lg"
|
||||
mb={4}
|
||||
boxShadow="md"
|
||||
bg={useColorModeValue('blue.50', 'blue.900')}
|
||||
bg={alertBgColor}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue('blue.200', 'blue.700')}
|
||||
borderColor={alertBorderColor}
|
||||
>
|
||||
<AlertIcon />
|
||||
<Box flex="1">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { logger } from '../../utils/logger';
|
||||
import defaultEventImage from '../../assets/img/default-event.jpg';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
@@ -174,7 +175,7 @@ const ConceptCenter = () => {
|
||||
const [stockMarketData, setStockMarketData] = useState({});
|
||||
const [loadingStockData, setLoadingStockData] = useState(false);
|
||||
// 默认图片路径
|
||||
const defaultImage = '/assets/img/default-event.jpg';
|
||||
const defaultImage = defaultEventImage;
|
||||
|
||||
// 获取最新交易日期
|
||||
const fetchLatestTradeDate = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user