| web-app/index.html | ●●●●● patch | view | raw | blame | history | |
| web-app/nginx-melody.conf | ●●●●● patch | view | raw | blame | history | |
| web-app/src/components/common/OutboundCallWidget.jsx | ●●●●● patch | view | raw | blame | history | |
| web-app/src/components/dashboard/TopSection.jsx | ●●●●● patch | view | raw | blame | history | |
| web-app/src/config/env.js | ●●●●● patch | view | raw | blame | history | |
| web-app/src/contexts/CaseDataContext.jsx | ●●●●● patch | view | raw | blame | history | |
| web-app/src/services/EvidenceAPIService.js | ●●●●● patch | view | raw | blame | history | |
| web-app/src/services/MediationAgreementAPIService.js | ●●●●● patch | view | raw | blame | history | |
| web-app/src/services/ProcessAPIService.js | ●●●●● patch | view | raw | blame | history |
web-app/index.html
New file @@ -0,0 +1 @@ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.feba8a67.js"></script><link href="/static/css/main.fcdca725.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> web-app/nginx-melody.conf
New file @@ -0,0 +1,111 @@ # ============================================ # 客户系统 Nginx配置文件 # 服务器: 36.140.67.217 # 部署路径: /var/www/customer-system # # 安装方法: # 1. 将此文件复制到服务器: /etc/nginx/conf.d/customer-system.conf # 2. 测试配置: nginx -t # 3. 重启服务: systemctl restart nginx # ============================================ server { listen 9002; server_name 36.140.67.217 localhost; # 网站根目录 root /deploy/code/front; index index.html index.htm; # 日志配置 access_log /var/log/nginx/customer-system.access.log; error_log /var/log/nginx/customer-system.error.log; # Gzip压缩配置 gzip on; gzip_vary on; gzip_min_length 1024; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json application/x-font-ttf application/x-font-woff image/svg+xml; # 静态资源缓存 - JS/CSS/字体/图片长期缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; # 允许跨域访问字体文件 add_header Access-Control-Allow-Origin *; } # HTML文件不缓存 - 确保用户总是获取最新版本 location ~* \.html$ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Pragma "no-cache"; } location /api/ { proxy_pass http://127.0.0.1:9031; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # SPA单页应用路由支持 # 所有路由都返回index.html,由前端路由处理 location / { try_files $uri $uri/ /index.html; } # 安全头配置 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # 禁止访问隐藏文件 location ~ /\. { deny all; access_log off; log_not_found off; } # 错误页面 error_page 404 /index.html; error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } # ============================================ # HTTPS配置 (可选 - 需要SSL证书) # ============================================ # server { # listen 443 ssl http2; # server_name 36.140.67.217; # # ssl_certificate /etc/nginx/ssl/customer-system.crt; # ssl_certificate_key /etc/nginx/ssl/customer-system.key; # ssl_session_timeout 1d; # ssl_session_cache shared:SSL:50m; # ssl_protocols TLSv1.2 TLSv1.3; # ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; # ssl_prefer_server_ciphers off; # # # ... 其他配置同上 # } # # # HTTP重定向到HTTPS # server { # listen 80; # server_name 36.140.67.217; # return 301 https://$server_name$request_uri; # } web-app/src/components/common/OutboundCallWidget.jsx
@@ -28,11 +28,10 @@ */ const OutboundCallWidget = ({ onSwitchTab, onRefreshData }) => { const { caseData } = useCaseData(); const [isVisible, setIsVisible] = useState(false); // 默认不显示,有任务时自动显示 const [isVisible, setIsVisible] = useState(false); // 默认隐藏 const [isMinimized, setIsMinimized] = useState(false); // 默认展开(非最小化) const [calls, setCalls] = useState([]); const isMountedRef = useRef(true); const fetchCallStatusRef = useRef(null); // 用于存储最新的 fetchCallStatus 函数引用 // 轮询间隔(毫秒) const POLL_INTERVAL = 10000; // 10秒 @@ -40,7 +39,7 @@ // 最大重试次数 const MAX_RETRY_COUNT = 10; // 获取 caseIdg // 获取 caseId const caseId = caseData?.caseId || caseData?.case_id; // 格式化通话时长 @@ -59,12 +58,11 @@ // 读取成功的任务 const storedSuccess = localStorage.getItem(OUTBOUND_JOBS_KEY); const successJobs = storedSuccess ? JSON.parse(storedSuccess) : []; console.log('读取成功任务:', successJobs.length, successJobs); // 读取失败的任务 const storedFailed = localStorage.getItem(`${OUTBOUND_JOBS_KEY}_failed`); const failedJobs = storedFailed ? JSON.parse(storedFailed) : []; console.log('读取失败任务:', failedJobs.length, failedJobs); console.log('读取失败任务:', failedJobs); // 清理失败任务 - 按 errorCode 不同策略 const now = Date.now(); @@ -215,40 +213,22 @@ * 查询通话状态 */ const fetchCallStatus = useCallback(async () => { console.log('fetchCallStatus 被调用'); // 从 localStorage 读取任务 const storedJobs = loadJobsFromStorage(); console.log('从 localStorage 读取的所有任务:', storedJobs); // 分离成功任务和失败任务 const successJobs = storedJobs.filter(job => !job.errorCode && isActiveStatus(job.callStatus)); const failedJobs = storedJobs.filter(job => job.errorCode > 0); console.log('成功任务数量:', successJobs.length, '失败任务数量:', failedJobs.length); if (successJobs.length === 0) { // 没有活跃任务,更新状态并返回 if (isMountedRef.current) { setCalls([...failedJobs]); if (failedJobs.length > 0) { if (failedJobs.length > 0 && !isVisible) { setIsVisible(true); } } return; } // 立即显示气泡(不等待 API 返回) console.log('isMountedRef.current:', isMountedRef.current); if (isMountedRef.current) { // 先合并当前的成功任务和失败任务,立即显示 const immediateJobs = [...successJobs, ...failedJobs.filter(f => !successJobs.some(s => s.personId === f.personId))]; console.log('准备设置 calls:', immediateJobs); setCalls(immediateJobs); setIsVisible(true); console.log('立即显示气泡,任务数:', immediateJobs.length, 'isVisible 设置为 true'); } else { console.log('组件未挂载,跳过显示气泡'); } // 收集需要更新到后端的任务(状态变化且原状态不是Scheduling) @@ -340,32 +320,21 @@ // 保存到 localStorage saveJobsToStorage(cleanedJobs); // 合并成功任务和失败任务,按 personId 去重(成功任务优先) const successPersonIds = new Set(cleanedJobs.map(job => job.personId)); // 过滤掉已有成功任务的 personId 对应的失败任务 const filteredFailedJobs = failedJobs.filter(job => !successPersonIds.has(job.personId)); const allJobs = [...cleanedJobs, ...filteredFailedJobs]; // 合并成功任务和失败任务 const allJobs = [...cleanedJobs, ...failedJobs]; // 更新组件状态 if (isMountedRef.current) { setCalls(allJobs); // 如果有任务,显示气泡(使用函数式更新避免依赖 isVisible) if (allJobs.length > 0) { // 如果有任务,显示气泡 if (allJobs.length > 0 && !isVisible) { setIsVisible(true); } } }, [triggerPageUpdate]); // 将最新的 fetchCallStatus 存储到 ref 中 useEffect(() => { fetchCallStatusRef.current = fetchCallStatus; }, [fetchCallStatus]); }, [isVisible, triggerPageUpdate]); // 定时轮询通话状态 useEffect(() => { // 组件挂载时设置为 true isMountedRef.current = true; // 初始加载 fetchCallStatus(); @@ -378,39 +347,6 @@ isMountedRef.current = false; }; }, [fetchCallStatus]); // 监听 localStorage 变化,外呼成功后立即刷新 useEffect(() => { const handleStorageChange = (e) => { // 监听外呼任务存储的变化 if (e.key === OUTBOUND_JOBS_KEY || e.key === `${OUTBOUND_JOBS_KEY}_failed`) { console.log('localStorage 变化,刷新外呼状态'); if (fetchCallStatusRef.current) { fetchCallStatusRef.current(); } } }; // 监听 storage 事件(跨标签页同步) window.addEventListener('storage', handleStorageChange); // 同页面内的 localStorage 变化需要手动触发 // 创建自定义事件监听 - 使用 ref 避免依赖问题 const handleCustomStorageChange = () => { console.log('同页面 localStorage 变化,刷新外呼状态'); if (fetchCallStatusRef.current) { fetchCallStatusRef.current(); } }; window.addEventListener('outbound-jobs-updated', handleCustomStorageChange); console.log('事件监听器已设置: outbound-jobs-updated'); return () => { window.removeEventListener('storage', handleStorageChange); window.removeEventListener('outbound-jobs-updated', handleCustomStorageChange); }; }, []); // 空依赖数组,只在组件挂载时设置一次 // 关闭气泡 const handleClose = (e) => { @@ -448,15 +384,6 @@ // 其他状态的任务正常显示 return true; }); // 添加调试日志 console.log('渲染检查 - calls:', calls.length, 'activeCalls:', activeCalls.length, 'isVisible:', isVisible, 'isMinimized:', isMinimized); // 如果没有任务,不渲染任何内容 if (activeCalls.length === 0) { console.log('无活跃任务,不渲染气泡'); return null; } // 如果最小化,显示AI客服图标 if (isMinimized) { @@ -659,6 +586,64 @@ </div> ))} {/* 无通话时的占位提示 */} {activeCalls.length === 0 && isVisible && ( <div style={{ background: 'linear-gradient(135deg, #1A6FB8 0%, #0d4a8a 100%)', borderRadius: 12, padding: '16px 20px', color: 'white', boxShadow: '0 4px 16px rgba(26, 111, 184, 0.3)', position: 'relative', minWidth: 280, }} > {/* 关闭按钮 */} <button onClick={handleClose} style={{ position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: '50%', border: 'none', background: 'rgba(255,255,255,0.2)', color: 'white', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, }} > <i className="fas fa-times" /> </button> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(255,255,255,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > <i className="fas fa-robot" style={{ fontSize: 18 }} /> </div> <div> <div style={{ fontSize: 16, fontWeight: 600 }}>智能外呼系统</div> <div style={{ fontSize: 13, opacity: 0.85 }}>暂无进行中的通话</div> </div> </div> </div> )} {/* CSS 动画 */} <style>{` @keyframes pulse { web-app/src/components/dashboard/TopSection.jsx
@@ -24,7 +24,7 @@ <img style={{ width: 36 }} src="http://gz.hugeinfo.com.cn/dyh/wx414ae04ac3f10b4e/images/pngAI_logo.png" alt="解纷智能体" // alt="云小调" /> </div> <div className="title-text"> web-app/src/config/env.js
@@ -25,7 +25,7 @@ }, [ENV_TYPES.SIT]: { // 集成测试环境 - 通过 Nginx 代理访问后端,避免跨域 baseURL: '', baseURL: 'http://localhost:9015', timeout: 30000, withCredentials: true, name: '集成测试环境' web-app/src/contexts/CaseDataContext.jsx
@@ -130,10 +130,6 @@ }); // 处理响应 console.log('triggerOutboundCall API 响应:', response); console.log('response.data:', response?.data); console.log('response.data 是数组:', Array.isArray(response?.data)); if (response?.data && Array.isArray(response.data)) { const successJobs = []; const failedJobs = []; @@ -149,7 +145,7 @@ caseId: String(caseId), // 添加 caseId 字段用于轮询 perClassName: item.perClassName || '', // 添加人员类型名称 trueName: item.trueName || '', // 添加真实姓名 startTime: item.createTime || item.createdTime || item.start_time, startTime: item.createdTime || item.start_time, pollStartTime: Date.now(), retryCount: 0 }); @@ -167,16 +163,8 @@ // 存储成功的任务到 localStorage if (successJobs.length > 0) { // 读取现有的成功任务,合并而不是覆盖 const existingJobs = JSON.parse(localStorage.getItem(OUTBOUND_JOBS_KEY) || '[]'); // 按 jobId 去重,新任务优先 const existingJobIds = new Set(successJobs.map(job => job.jobId)); const filteredExistingJobs = existingJobs.filter(job => !existingJobIds.has(job.jobId)); const mergedJobs = [...successJobs, ...filteredExistingJobs]; localStorage.setItem(OUTBOUND_JOBS_KEY, JSON.stringify(mergedJobs)); console.log('存储外呼任务成功,新增:', successJobs.length, '总数:', mergedJobs.length); console.log('存储的任务:', JSON.stringify(mergedJobs, null, 2)); localStorage.setItem(OUTBOUND_JOBS_KEY, JSON.stringify(successJobs)); console.log('存储外呼任务成功,数量:', successJobs.length); // 外呼成功后,清除对应的失败记录 const storedFailedJobs = JSON.parse(localStorage.getItem(`${OUTBOUND_JOBS_KEY}_failed`) || '[]'); @@ -188,12 +176,6 @@ localStorage.setItem(`${OUTBOUND_JOBS_KEY}_failed`, JSON.stringify(remainingFailedJobs)); console.log('外呼成功后清除失败记录,清除数量:', storedFailedJobs.length - remainingFailedJobs.length); } // 延迟触发自定义事件,确保监听器已经设置好 setTimeout(() => { console.log('触发 outbound-jobs-updated 事件'); window.dispatchEvent(new CustomEvent('outbound-jobs-updated')); }, 300); } // 存储失败的任务到 localStorage(用于气泡显示) @@ -223,9 +205,6 @@ localStorage.setItem(`${OUTBOUND_JOBS_KEY}_failed`, JSON.stringify(cleanedFailedJobs)); console.log('存储外呼失败任务,数量:', cleanedFailedJobs.length); // 触发自定义事件通知 OutboundCallWidget 组件刷新 window.dispatchEvent(new CustomEvent('outbound-jobs-updated')); } // 提示失败的任务 @@ -389,6 +368,10 @@ return; } EvidenceAPIService.processCaseFilesOcr(params.caseId).catch((ocrError) => { console.error('触发案件文件OCR失败:', ocrError); }); try { await OutboundBotAPIService.syncStatusByCase({ caseId: params.caseId }); } catch (syncError) { @@ -405,8 +388,7 @@ params.caseId, { caseTypeFirst: params.caseTypeFirst, platform_code: params.platform_code, authorization: params.auth_token platform_code: params.platform_code } ); web-app/src/services/EvidenceAPIService.js
@@ -8,6 +8,19 @@ class EvidenceAPIService { /** * 触发案件关联文件的 OCR 处理(后台任务) * POST /api/v1/case-files-ocr/process * @param {string|number} caseId - 案件ID * @returns {Promise} OCR处理结果 */ static processCaseFilesOcr(caseId) { const trimmedCaseId = String(caseId ?? '').trim(); const formData = new FormData(); formData.append('caseId', trimmedCaseId); return request.post('/api/v1/case-files-ocr/process', formData); } /** * 证据列表查询 * GET /api/v1/evidence/list * @param {Object} params - 查询参数 web-app/src/services/MediationAgreementAPIService.js
@@ -68,7 +68,7 @@ * @returns {Promise} 重新生成的协议信息(包含新的agreeId和agreeContent) */ static regenerateAgreement(caseId) { return request.post('/api/v1/medi-agreement/regenerate', { caseId }, { timeout: 120000 }); return request.post('/api/v1/medi-agreement/regenerate', { caseId }, { timeout: 30000 }); } } web-app/src/services/ProcessAPIService.js
@@ -119,7 +119,7 @@ } /** * 人工接管API· * 人工接管API * PUT /api/v1/mediation-timeline/v2/case/{caseId}/takeover * @param {string} caseId - 案件ID * @param {Object} data - 请求数据