// ==UserScript== // @name 终极CRM半自动外呼循环(话术循环切换版) // @namespace http://tampermonkey.net/ // @version 12.20 // @description 自动拨号、话术循环切换(用/分隔)、双击回车聚焦、键盘防误触锁定、内置操作手册。 // @match https://zrfw.sxl.cdsxyc.cn/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; console.log('🚀 话术循环切换版已加载...'); // ========================================== // 1. 设置与配置管理 // ========================================== let settings = { scripts: { script1: "通话中/留言", script2: "长响/未接听", script3: "不需要/贷挂" }, dialDelay: { enabled: true, min: 1000, max: 3000 }, dailyGoal: 200, keybinds: { stop: 'S', pause: 'P', script1: '1', script2: '2', script3: '3' }, logConfig: { info: true, success: true, warn: true, error: true, fold: false } }; // 🆕 记录每个话术当前的循环索引 let scriptCycles = { script1: 0, script2: 0, script3: 0 }; function loadSettings() { const saved = localStorage.getItem('crm_advanced_settings'); if (saved) { try { const parsed = JSON.parse(saved); settings = deepMerge(settings, parsed); } catch (e) {} } window._CRMSettings = settings; updateKeybindsFromSettings(); } function saveSettings() { localStorage.setItem('crm_advanced_settings', JSON.stringify(settings)); window._CRMSettings = settings; updateKeybindsFromSettings(); } function deepMerge(target, source) { const output = Object.assign({}, target); if (isObject(target) && isObject(source)) { Object.keys(source).forEach(key => { if (isObject(source[key])) { if (!(key in target)) Object.assign(output, { [key]: source[key] }); else output[key] = deepMerge(target[key], source[key]); } else { Object.assign(output, { [key]: source[key] }); } }); } return output; } function isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)); } loadSettings(); const SELECTORS = { phoneNum: 'span.callDisabled', saveBtnText: '保存', nextBtnText: '下一位' }; let isRunning = false; let stopFlag = false; let isPaused = false; let currentTimeout = null; let lastProcessedPhone = ''; let isGoalAchieved = false; let isKeyboardLocked = false; // ========================================== // 2. 全局 Toast 提示系统 // ========================================== function showToast(message, type = 'info', duration = 2500) { let toast = document.getElementById('crm-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'crm-toast'; toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 99999; background: rgba(0,0,0,0.8); color: #fff; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-family: system-ui, sans-serif; box-shadow: 0 8px 24px rgba(0,0,0,0.3); opacity: 0; transition: opacity 0.3s ease; pointer-events: none; `; document.body.appendChild(toast); } let icon = type === 'success' ? '✅' : (type === 'error' ? '❌' : (type === 'warn' ? '⚠️' : 'ℹ️')); toast.innerHTML = `${icon} ${message}`; toast.style.opacity = '1'; clearTimeout(toast._hideTimer); toast._hideTimer = setTimeout(() => { toast.style.opacity = '0'; }, duration); } // ========================================== // 3. 【内置操作手册】渲染组件 // ========================================== const MANUAL_HTML = `

📖 终极CRM半自动外呼循环 - 操作手册

当前版本: v12.20 | 新增: 快捷话术循环切换(用 / 分隔)

🚀 快速启动

⌨️ 核心快捷键(全键盘操作)

📊 辅助工具

⚙️ 设置面板

🧹 数据安全与维护

`; // ========================================== // 4. 可视化设置面板 // ========================================== function openSettingsModal() { const modal = document.createElement('div'); modal.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);z-index:99999;display:flex;justify-content:center;align-items:center;font-family:system-ui,sans-serif;'; const box = document.createElement('div'); box.style.cssText = 'background:#fff;width:600px;max-height:80vh;overflow-y:auto;padding:24px;border-radius:16px;box-shadow:0 20px 48px rgba(0,0,0,0.2);'; box.innerHTML = `

⚙️ 全局设置

话术A
话术B
话术C
最小: ms
最大: ms
今日目标拨号数:
紧急停止: (Ctrl+Shift)
暂停/继续: (Ctrl+Shift)
💡 Ctrl+Shift+Enter 一键保存
💡 Ctrl+Shift+Backspace 一键清空备注框
💡 Ctrl+Shift+L 锁定/解锁 键盘防误触模式
📝 话术快捷键(触发 Ctrl+Alt):
话术A: (Ctrl+Alt+数字)
话术B: (Ctrl+Alt+数字)
话术C: (Ctrl+Alt+数字)
`; modal.appendChild(box); document.body.appendChild(modal); document.getElementById('sett_close_btn').onclick = () => modal.remove(); document.getElementById('sett_save_btn').onclick = () => { settings.scripts.script1 = document.getElementById('sett_script1').value; settings.scripts.script2 = document.getElementById('sett_script2').value; settings.scripts.script3 = document.getElementById('sett_script3').value; settings.dialDelay.enabled = document.getElementById('sett_delay_enable').checked; settings.dialDelay.min = parseInt(document.getElementById('sett_delay_min').value) || 1000; settings.dialDelay.max = parseInt(document.getElementById('sett_delay_max').value) || 3000; settings.dailyGoal = parseInt(document.getElementById('sett_goal').value) || 200; settings.logConfig.info = document.getElementById('sett_log_info').checked; settings.logConfig.success = document.getElementById('sett_log_success').checked; settings.logConfig.warn = document.getElementById('sett_log_warn').checked; settings.logConfig.error = document.getElementById('sett_log_error').checked; settings.logConfig.fold = document.getElementById('sett_log_fold').checked; settings.keybinds.stop = document.getElementById('sett_key_stop').value.toUpperCase(); settings.keybinds.pause = document.getElementById('sett_key_pause').value.toUpperCase(); settings.keybinds.script1 = document.getElementById('sett_key_s1').value.toUpperCase(); settings.keybinds.script2 = document.getElementById('sett_key_s2').value.toUpperCase(); settings.keybinds.script3 = document.getElementById('sett_key_s3').value.toUpperCase(); saveSettings(); modal.remove(); updateStatsUI(); if (logWindow && logWindow.style.display === 'flex') { renderLogContent(searchInput.value); } showToast('✅ 设置已保存', 'success'); }; document.getElementById('sett_export').onclick = () => { navigator.clipboard.writeText(btoa(unescape(encodeURIComponent(JSON.stringify(settings))))).then(() => showToast('📤 配置已复制', 'success')); }; document.getElementById('sett_import').onclick = () => { const input = prompt("粘贴配置:"); if (input) { try { settings = deepMerge(settings, JSON.parse(decodeURIComponent(escape(atob(input))))); saveSettings(); showToast('✅ 导入成功', 'success'); modal.remove(); } catch (e) { showToast('❌ 配置无效', 'error'); } } }; document.getElementById('sett_manual').onclick = () => { const manualModal = document.createElement('div'); manualModal.className = 'manual-modal'; manualModal.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.5);backdrop-filter:blur(3px);z-index:100000;display:flex;justify-content:center;align-items:center;font-family:system-ui,sans-serif;'; const manualBox = document.createElement('div'); manualBox.style.cssText = 'background:#fff;width:650px;max-height:80vh;border-radius:16px;box-shadow:0 20px 48px rgba(0,0,0,0.25);padding:24px;overflow:hidden;position:relative;'; manualBox.innerHTML = MANUAL_HTML; manualModal.appendChild(manualBox); document.body.appendChild(manualModal); const closeManualBtn = manualBox.querySelector('#manual_close_btn'); if (closeManualBtn) { closeManualBtn.onclick = () => manualModal.remove(); } manualModal.addEventListener('click', (e) => { if (e.target === manualModal) manualModal.remove(); }); }; } function updateKeybindsFromSettings() { window._keyStop = settings.keybinds.stop; window._keyPause = settings.keybinds.pause; window._keyScript1 = settings.keybinds.script1; window._keyScript2 = settings.keybinds.script2; window._keyScript3 = settings.keybinds.script3; } // ========================================== // 5. 极简核心 UI 组件 // ========================================== const panel = document.createElement('div'); panel.id = 'crm-panel'; panel.style.cssText = ` position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: #fff; border: 1px solid #ccc; border-radius: 8px; padding: 14px 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); display: flex; flex-direction: column; gap: 8px; width: 225px; font-family: system-ui, sans-serif; cursor: grab; user-select: none; `; const header = document.createElement('div'); header.style.cssText = `display: flex; justify-content: flex-end; align-items: center; width: 100%; gap: 4px; margin-bottom: 4px;`; const statusText = document.createElement('span'); statusText.style.display = 'none'; const logBtn = document.createElement('span'); logBtn.innerText = '📋 日志'; logBtn.style.cssText = 'cursor:pointer;color:#2196F3;font-size:12px;font-weight:500;padding:2px 8px;'; const darkModeBtn = document.createElement('span'); darkModeBtn.innerText = '🌙'; darkModeBtn.style.cssText = 'cursor:pointer;font-size:14px;padding:2px 6px;'; darkModeBtn.onclick = () => { document.body.classList.toggle('crm-dark-mode'); darkModeBtn.innerText = document.body.classList.contains('crm-dark-mode') ? '☀️' : '🌙'; }; const hideBtn = document.createElement('span'); hideBtn.innerText = '—'; hideBtn.style.cssText = 'cursor:pointer;color:#999;font-weight:bold;font-size:16px;padding:0 6px;'; header.appendChild(statusText); header.appendChild(logBtn); header.appendChild(darkModeBtn); header.appendChild(hideBtn); const controlsWrap = document.createElement('div'); controlsWrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;'; function createBtn(text, bg, color) { const btn = document.createElement('button'); btn.innerText = text; btn.style.cssText = `background:${bg};color:${color};border:none;padding:7px 0;border-radius:6px;cursor:pointer;font-weight:bold;font-size:13px;flex:1 1 45%;min-width:80px;`; return btn; } const startBtn = createBtn('▶ 开始循环', '#4CAF50', '#fff'); const pauseBtn = createBtn('⏸️ 暂停', '#2196F3', '#fff'); const stopBtn = createBtn('⏹ 紧急停止', '#f44336', '#fff'); const clearBtn = createBtn('⚡ 强制清除弹窗', '#ff9800', '#fff'); const scriptBtn1 = createBtn('📝 话术A', '#8BC34A', '#fff'); const scriptBtn2 = createBtn('📝 话术B', '#8BC34A', '#fff'); const scriptBtn3 = createBtn('📝 话术C', '#8BC34A', '#fff'); const settingsBtn = createBtn('⚙️ 设置', '#9E9E9E', '#fff'); const statsBtn = createBtn('📊 战绩', '#607D8B', '#fff'); controlsWrap.appendChild(startBtn); controlsWrap.appendChild(pauseBtn); controlsWrap.appendChild(stopBtn); controlsWrap.appendChild(clearBtn); controlsWrap.appendChild(scriptBtn1); controlsWrap.appendChild(scriptBtn2); controlsWrap.appendChild(scriptBtn3); controlsWrap.appendChild(settingsBtn); controlsWrap.appendChild(statsBtn); panel.appendChild(header); panel.appendChild(controlsWrap); document.body.appendChild(panel); // 唤醒按钮(呼吸灯载体) const toggleBtn = document.createElement('div'); toggleBtn.innerText = '📞 面板'; toggleBtn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; z-index: 9998; background: #2196F3; color: white; padding: 8px 14px; border-radius: 20px; font-weight: bold; font-size: 12px; box-shadow: 0 4px 12px rgba(33,150,243,0.3); cursor: pointer; display: none; transition: all 0.2s ease; `; toggleBtn.onmouseenter = () => { toggleBtn.style.transform = 'scale(1.05)'; }; toggleBtn.onmouseleave = () => { toggleBtn.style.transform = 'scale(1)'; }; document.body.appendChild(toggleBtn); // 呼吸灯 CSS 动画 const breathStyle = document.createElement('style'); breathStyle.id = 'crm-breath-style'; breathStyle.textContent = ` @keyframes breath-running { 0% { box-shadow: 0 4px 12px rgba(33,150,243,0.2); transform: scale(1); } 50% { box-shadow: 0 8px 24px rgba(33,150,243,0.6); transform: scale(1.05); } 100% { box-shadow: 0 4px 12px rgba(33,150,243,0.2); transform: scale(1); } } @keyframes breath-paused { 0% { opacity: 0.4; box-shadow: 0 2px 6px rgba(255,193,7,0.2); } 50% { opacity: 0.7; box-shadow: 0 6px 16px rgba(255,193,7,0.4); } 100% { opacity: 0.4; box-shadow: 0 2px 6px rgba(255,193,7,0.2); } } .toggler-running { animation: breath-running 2s infinite ease-in-out !important; } .toggler-paused { animation: breath-paused 3s infinite ease-in-out !important; background: #FFC107 !important; } `; document.head.appendChild(breathStyle); // 暗黑模式 CSS const style = document.createElement('style'); style.id = 'crm-dark-mode-style'; style.textContent = ` .crm-dark-mode #crm-panel { background: #252526 !important; border-color: #444 !important; color: #e0e0e0 !important; } .crm-dark-mode #crm-panel button { border: 1px solid rgba(255,255,255,0.05) !important; } .crm-dark-mode #crm-log-window { background: #252526 !important; border-color: #444 !important; } .crm-dark-mode #crm-log-window input, .crm-dark-mode #crm-log-window button { background: #2d2d2d !important; color: #ddd !important; border-color: #444 !important; } .crm-dark-mode #crm-log-window .log-content { background: #1a1a1a !important; } `; document.head.appendChild(style); // ========================================== // 6. 🔥【全局灰屏斩杀者】DOM 监控器 // ========================================== const overlayKiller = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { if (node.matches && (node.matches('.el-overlay, .v-modal, .el-loading-mask') || node.querySelector && (node.querySelector('.el-overlay, .v-modal, .el-loading-mask')))) { try { if (node.matches('.el-overlay, .v-modal, .el-loading-mask')) { node.remove(); } else { const overlay = node.querySelector('.el-overlay, .v-modal, .el-loading-mask'); if (overlay) overlay.remove(); } } catch (e) {} } } }); if (mutation.target && mutation.target.matches && mutation.target.matches('.el-overlay, .v-modal, .el-loading-mask')) { mutation.target.remove(); } }); }); setTimeout(() => { overlayKiller.observe(document.body, { childList: true, subtree: true, attributes: true }); }, 1000); // ========================================== // 7. 日志系统与统计 // ========================================== const logs = []; const MAX_LOG_ENTRIES = 500; let stats = { dialed: 0, saved: 0 }; let lastDate = new Date().toDateString(); const logWindow = document.createElement('div'); logWindow.id = 'crm-log-window'; logWindow.style.cssText = `position:fixed;bottom:80px;left:20px;z-index:9999;width:450px;height:340px;background:#fff;border:1px solid #ccc;border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,0.1);display:flex;flex-direction:column;display:none;font-family:system-ui,sans-serif;overflow:hidden;`; const logHeader = document.createElement('div'); logHeader.style.cssText = 'padding:12px 14px;border-bottom:1px solid #eee;display:flex;flex-direction:column;cursor:grab;background:#fafafa;border-radius:10px 10px 0 0;'; const logHeaderTitle = document.createElement('div'); logHeaderTitle.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;'; logHeaderTitle.innerText = '📋 执行日志'; const statsContainer = document.createElement('div'); statsContainer.style.cssText = 'color:#555;font-size:12px;padding:4px 0;border-top:1px solid #eee;border-bottom:1px solid #eee;margin-bottom:4px;background:#fff;border-radius:4px;'; const logControls = document.createElement('div'); logControls.style.cssText = 'display:flex;gap:6px;align-items:center;margin-top:4px;flex-wrap:wrap;'; const searchWrapper = document.createElement('div'); searchWrapper.style.cssText = 'position:relative;flex:1;display:flex;align-items:center;min-width:80px;'; const searchInput = document.createElement('input'); searchInput.placeholder = '🔍 搜索内容...'; searchInput.style.cssText = 'flex:1;border:1px solid #ccc;border-radius:6px;padding:4px 10px;font-size:12px;min-width:70px;padding-right:24px;'; const searchClearBtn = document.createElement('span'); searchClearBtn.innerText = '✕'; searchClearBtn.style.cssText = 'cursor:pointer;color:#aaa;font-size:14px;display:none;position:absolute;right:6px;'; searchClearBtn.onclick = () => { searchInput.value = ''; searchClearBtn.style.display = 'none'; renderLogContent(''); }; searchInput.oninput = (e) => { const val = e.target.value; renderLogContent(val); searchClearBtn.style.display = val.length > 0 ? 'block' : 'none'; }; searchWrapper.appendChild(searchInput); searchWrapper.appendChild(searchClearBtn); function exportLogs() { if (logs.length === 0) { showToast('没有日志可导出', 'warn'); return; } let content = '==== 外呼日志 ====\n'; logs.forEach(log => { content += `[${log.time}] [${log.type.toUpperCase()}] ${log.message}\n`; }); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `外呼日志_${new Date().toISOString().slice(0,10)}.txt`; a.click(); URL.revokeObjectURL(url); showToast('✅ 已导出日志', 'success'); } function resetStats() { stats.dialed = 0; stats.saved = 0; updateStatsUI(); saveStateToLocal(); showToast('🔄 已重置数据', 'info'); } const exportBtn = document.createElement('button'); exportBtn.innerText = '导出'; exportBtn.style.cssText = 'background:#4CAF50;color:white;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:12px;font-weight:bold;'; exportBtn.onclick = exportLogs; const resetBtn = document.createElement('button'); resetBtn.innerText = '🔄 重置'; resetBtn.style.cssText = 'background:#eee;border:1px solid #ccc;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;'; resetBtn.onclick = resetStats; const clearLogBtn = document.createElement('button'); clearLogBtn.innerText = '清空'; clearLogBtn.style.cssText = 'background:#eee;border:1px solid #ccc;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px;'; const closeLogBtn = document.createElement('button'); closeLogBtn.innerText = '✕'; closeLogBtn.style.cssText = 'background:none;border:none;cursor:pointer;font-size:14px;color:#999;'; const logContent = document.createElement('div'); logContent.className = 'log-content'; logContent.style.cssText = 'flex:1;overflow-y:auto;padding:8px 12px;font-size:12px;font-family:monospace;line-height:1.6;background:#f9f9f9;'; clearLogBtn.onclick = () => { logs.length = 0; saveStateToLocal(); renderLogContent(); showToast('已清空日志', 'info'); if(searchInput.value.trim() !== '') { searchInput.value = ''; searchClearBtn.style.display = 'none'; } }; closeLogBtn.onclick = () => { logWindow.style.display = 'none'; saveLogState(); }; logControls.appendChild(searchWrapper); logControls.appendChild(exportBtn); logControls.appendChild(resetBtn); logControls.appendChild(clearLogBtn); logControls.appendChild(closeLogBtn); logHeader.appendChild(logHeaderTitle); logHeader.appendChild(statsContainer); logHeader.appendChild(logControls); logWindow.appendChild(logHeader); logWindow.appendChild(logContent); document.body.appendChild(logWindow); // 双击日志标题栏,一键关闭日志 logHeader.addEventListener('dblclick', (e) => { if (logWindow.style.display !== 'none') { logWindow.style.display = 'none'; saveLogState(); } }); function saveStateToLocal() { try { const state = { logs, stats, lastDate }; localStorage.setItem('crm_full_state', JSON.stringify(state)); localStorage.setItem('crm_full_state_backup', JSON.stringify(state)); } catch (e) {} } function loadStateFromLocal() { const saved = localStorage.getItem('crm_full_state'); if (saved) { try { const state = JSON.parse(saved); logs.push(...state.logs); stats.dialed = state.stats.dialed || 0; stats.saved = state.stats.saved || 0; if (state.lastDate) { lastDate = state.lastDate; } updateStatsUI(); if (logWindow.style.display === 'flex') renderLogContent(); } catch (e) {} } } function addLog(message, type = 'info', addCustomerInfo = false) { const time = new Date().toLocaleTimeString(); let finalMsg = message; if (addCustomerInfo) { const customer = getCurrentCustomerInfo(); finalMsg = `[${customer.name} ${customer.phone}] ${message}`; } logs.push({ time, message: finalMsg, type }); if (logs.length > MAX_LOG_ENTRIES) logs.shift(); saveStateToLocal(); if (logWindow && logWindow.style.display === 'flex') { renderLogContent(searchInput.value); } updateStatsUI(); } function renderLogContent(filterText = '') { if (!logContent) return; logContent.innerHTML = ''; let filteredLogs = filterText ? logs.filter(log => log.message.toLowerCase().includes(filterText.toLowerCase()) || log.time.includes(filterText)) : logs; filteredLogs = filteredLogs.filter(log => settings.logConfig[log.type]); let finalLogs = filteredLogs; if (settings.logConfig.fold) { const foldedMap = new Map(); for (const log of filteredLogs) { const match = log.message.match(/^\[([^\]]+)\]/); if (!match) { foldedMap.set(log.time, log); continue; } const customerKey = match[1]; if (log.message.includes('📝 已录入跟进内容')) { foldedMap.set(customerKey, log); } } finalLogs = Array.from(foldedMap.values()); finalLogs.sort((a, b) => a.time.localeCompare(b.time)); } if (finalLogs.length === 0) { logContent.innerHTML = '
无记录
'; } else { finalLogs.forEach(log => { const line = document.createElement('div'); line.style.cssText = 'padding:2px 0;border-bottom:1px solid #eee;'; line.innerHTML = `${log.time}${log.message}`; logContent.appendChild(line); }); } logContent.scrollTop = logContent.scrollHeight; } function updateStatsUI() { if (statsContainer) { const goal = settings.dailyGoal || 200; const done = stats.dialed; const progress = Math.min(100, Math.round((done / goal) * 100)); statsContainer.innerHTML = `
🎯 ${done} / ${goal} (${progress}%) 📞 ${stats.dialed} | ✅ ${stats.saved}
`; if (settings.dailyGoal > 0 && done >= settings.dailyGoal && !isGoalAchieved) { isGoalAchieved = true; showToast('🎉 已达成今日目标,准备下班吧!', 'success', 4000); if (toggleBtn.style.display !== 'none') { toggleBtn.style.animation = 'none'; toggleBtn.style.transition = 'opacity 0.1s'; let count = 0; const flashInterval = setInterval(() => { toggleBtn.style.opacity = count % 2 === 0 ? '0.2' : '1'; count++; if (count > 5) { clearInterval(flashInterval); toggleBtn.style.transition = 'all 0.2s ease'; toggleBtn.style.opacity = '1'; updateTogglerStatus(); } }, 200); } } } } function saveLogState() { localStorage.setItem('crm_log_display', logWindow.style.display); const left = parseInt(logWindow.style.left); const top = parseInt(logWindow.style.top); localStorage.setItem('crm_log_left', isNaN(left) ? '20' : String(left)); localStorage.setItem('crm_log_top', isNaN(top) ? '80' : String(top)); } (function restoreLogState() { const savedDisplay = localStorage.getItem('crm_log_display'); const savedLeft = parseInt(localStorage.getItem('crm_log_left')); const savedTop = parseInt(localStorage.getItem('crm_log_top')); if (savedDisplay === 'flex') { logWindow.style.display = 'flex'; if (!isNaN(savedLeft)) logWindow.style.left = savedLeft + 'px'; if (!isNaN(savedTop)) logWindow.style.top = savedTop + 'px'; renderLogContent(); updateStatsUI(); } })(); loadStateFromLocal(); logBtn.onclick = () => { if (logWindow.style.display === 'none') { logWindow.style.display = 'flex'; renderLogContent(); updateStatsUI(); saveLogState(); } else { logWindow.style.display = 'none'; saveLogState(); } }; // ========================================== // 8. 基础逻辑与交互 // ========================================== function getCurrentCustomerInfo() { let name = '未知客户'; let phone = '无号码'; try { const phoneSpan = document.querySelector(SELECTORS.phoneNum); if (phoneSpan) { phone = phoneSpan.innerText.trim(); const parentDiv = phoneSpan.closest('div[style*="margin-top: 6px;"]'); if (parentDiv) { const nameEl = parentDiv.parentElement?.querySelector('.el-tooltip.item, div[style*="font-weight: 600;"]'); if (nameEl) name = nameEl.innerText.trim(); } } } catch (e) {} return { name, phone }; } function sleep(ms) { return new Promise(resolve => { const start = Date.now(); const check = () => { if (stopFlag) { resolve(); return; } if (isPaused) { setTimeout(check, 200); return; } if (Date.now() - start < ms) { setTimeout(check, 200); } else { resolve(); } }; setTimeout(check, 100); }); } function forceRemovePopup() { const targets = document.querySelectorAll('.el-overlay, .el-dialog__wrapper, .v-modal, .el-loading-mask'); targets.forEach(el => { if (el.parentNode) { try { el.parentNode.removeChild(el); } catch (e) {} } el.style.cssText = 'display: none !important;'; }); document.body.classList.remove('el-popup-parent--hidden'); document.documentElement.style.cssText = 'overflow: auto !important; pointer-events: auto !important;'; document.body.style.cssText = 'overflow: auto !important; pointer-events: auto !important;'; return true; } function findClickableBtn(text) { let btns = document.querySelectorAll('button'); for (let btn of btns) { if (btn.offsetParent !== null && btn.textContent.trim() === text) { if (!btn.closest('.el-dropdown-menu')) return btn; } } return null; } function getFollowTextarea() { let el = document.querySelector('textarea.el-textarea__inner, textarea[placeholder="请输入跟进记录内容"]'); if (el) return el; const allTextareas = document.querySelectorAll('textarea'); for (let t of allTextareas) { if (t.placeholder && (t.placeholder.includes('跟进记录') || t.placeholder.includes('跟进备注'))) { return t; } } return null; } async function findDialBtn(retries = 10) { for (let i = 0; i < retries; i++) { if (stopFlag) return null; if (isPaused) await sleep(500); const phoneSpan = document.querySelector(SELECTORS.phoneNum); if (phoneSpan) { const parentDiv = phoneSpan.closest('div[style*="margin-top: 6px;"]'); if (parentDiv) { const dialBtn = parentDiv.querySelector('i.icon-kaishiboda'); if (dialBtn) return dialBtn; } const baseInfo = phoneSpan.closest('.base-info, .el-col, .customer-info'); if (baseInfo) { const dialBtn = baseInfo.querySelector('i.icon-kaishiboda'); if (dialBtn) return dialBtn; } } if (i < retries - 1) { await sleep(1000); } } return null; } async function waitForHangUp() { let elapsed = 0; while (elapsed < CONFIG.maxWaitTime) { if (stopFlag) return false; if (isPaused) await sleep(500); if (getFollowTextarea()) { return true; } await sleep(500); elapsed += 500; } return true; } async function waitForNext(currentPhone) { for (let i = 0; i < CONFIG.maxWaitForChange; i++) { if (stopFlag) return false; if (isPaused) await sleep(500); await sleep(1000); const newPhone = document.querySelector(SELECTORS.phoneNum); if (newPhone && newPhone.innerText.trim() !== currentPhone) return true; const nextBtn = findClickableBtn(SELECTORS.nextBtnText); if (nextBtn) { nextBtn.click(); } } return false; } let isDragging = false, offsetX = 0, offsetY = 0; panel.addEventListener('mousedown', (e) => { if (e.target.closest('button') || e.target === hideBtn || e.target === logBtn || e.target === darkModeBtn) return; isDragging = true; const rect = panel.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; panel.style.cursor = 'grabbing'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let left = e.clientX - offsetX, top = e.clientY - offsetY; left = Math.max(0, Math.min(left, window.innerWidth - panel.offsetWidth)); top = Math.max(0, Math.min(top, window.innerHeight - panel.offsetHeight)); panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.bottom = 'auto'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.cursor = 'grab'; } }); let logDragging = false, logOffsetX = 0, logOffsetY = 0; logHeader.addEventListener('mousedown', (e) => { if (e.target.closest('button, input, textarea')) return; logDragging = true; const rect = logWindow.getBoundingClientRect(); logOffsetX = e.clientX - rect.left; logOffsetY = e.clientY - rect.top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!logDragging) return; let left = e.clientX - logOffsetX, top = e.clientY - logOffsetY; left = Math.max(0, Math.min(left, window.innerWidth - logWindow.offsetWidth)); top = Math.max(0, Math.min(top, window.innerHeight - logWindow.offsetHeight)); logWindow.style.left = left + 'px'; logWindow.style.top = top + 'px'; logWindow.style.bottom = 'auto'; logWindow.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (logDragging) { logDragging = false; saveLogState(); } }); function updateStatus(text, color = '#333') { if (!statusText) return; statusText.innerText = text; statusText.style.color = color; } function updatePauseButtonState() { pauseBtn.innerText = isPaused ? '▶️ 继续' : '⏸️ 暂停'; pauseBtn.style.background = isPaused ? '#8BC34A' : '#2196F3'; } function showDailyStats() { const goal = settings.dailyGoal || 200; const done = stats.dialed; const saved = stats.saved; const progress = Math.min(100, Math.round((done / goal) * 100)); showToast(`📊 今日战绩: 已拨号 ${done} 个, 已保存 ${saved} 个, 完成率 ${progress}%`, 'info', 4000); addLog(`📊 用户查看了今日战绩: 拨号 ${done}, 保存 ${saved}, 进度 ${progress}%`, 'info'); } statsBtn.onclick = showDailyStats; // ========================================== // 9. 🔥【核心修改】循环切换话术逻辑 // ========================================== function insertScriptText(scriptKey) { const textarea = getFollowTextarea(); if (!textarea) { showToast('❌ 请先打开跟进记录弹窗', 'error'); return; } let rawText = settings.scripts[scriptKey]; let textToInsert = rawText; // 检查是否包含分隔符 '/',如果是,则进入循环切换逻辑 if (rawText.includes('/')) { const options = rawText.split('/').map(s => s.trim()).filter(s => s.length > 0); if (options.length > 0) { // 获取当前索引,如果不存在则初始化为0 const currentIndex = scriptCycles[scriptKey] || 0; textToInsert = options[currentIndex % options.length]; // 更新索引,准备下一次切换 scriptCycles[scriptKey] = (currentIndex + 1) % options.length; } } textarea.value = textToInsert; textarea.dispatchEvent(new Event('input', { bubbles: true })); const displayText = textToInsert.length > 50 ? textToInsert.substring(0, 50) + '...' : textToInsert; addLog(`📝 快捷键触发了话术${scriptKey.slice(-1)}: ${displayText}`, 'info', true); showToast(`📝 已填入话术${scriptKey.slice(-1)}`, 'success'); } scriptBtn1.onclick = () => { insertScriptText('script1'); }; scriptBtn2.onclick = () => { insertScriptText('script2'); }; scriptBtn3.onclick = () => { insertScriptText('script3'); }; settingsBtn.onclick = openSettingsModal; // ========================================== // 10. 核心自动化流程 // ========================================== const CONFIG = { maxWaitTime: 25000, maxWaitForChange: 20 }; async function autoProcess() { if (stopFlag) { isRunning = false; return; } if (isPaused) await sleep(500); const phoneElem = document.querySelector(SELECTORS.phoneNum); if (!phoneElem) { stopFlag = true; isRunning = false; return; } const currentPhone = phoneElem.innerText.trim(); if (!lastProcessedPhone) lastProcessedPhone = currentPhone; if (currentPhone === lastProcessedPhone && currentPhone !== '未检测到号码') { const nextBtn = findClickableBtn(SELECTORS.nextBtnText); if (nextBtn) nextBtn.click(); const changed = await waitForNext(currentPhone); if (!changed) { stopFlag = true; isRunning = false; return; } } const dialBtn = await findDialBtn(); if (!dialBtn) { stopFlag = true; isRunning = false; return; } if (settings.dialDelay.enabled && !stopFlag) { const delay = Math.floor(Math.random() * (settings.dialDelay.max - settings.dialDelay.min + 1)) + settings.dialDelay.min; await sleep(delay); } dialBtn.click(); stats.dialed++; updateStatsUI(); addLog('📞 已成功点击拨号图标', 'success', true); await waitForHangUp(); if (stopFlag) { isRunning = false; return; } const saved = await waitForSave(); if (!saved || stopFlag) { isRunning = false; return; } stats.saved++; updateStatsUI(); await sleep(1500); forceRemovePopup(); await sleep(800); let nextBtn = findClickableBtn(SELECTORS.nextBtnText); if (!nextBtn) { await sleep(1500); nextBtn = findClickableBtn(SELECTORS.nextBtnText); if (!nextBtn) { stopFlag = true; isRunning = false; return; } } nextBtn.click(); addLog('➡️ 已自动点击【下一位】,等待加载新客户', 'success', true); const isLoaded = await waitForNext(lastProcessedPhone); if (!isLoaded) { stopFlag = true; isRunning = false; return; } await sleep(1000); autoProcess(); } // ========================================== // 11. 监听保存与快捷键 // ========================================== function waitForSave() { return new Promise((resolve) => { let isResolved = false; let clickHandler, keydownHandler, checkStop; const cleanUp = () => { if (clickHandler) document.removeEventListener('click', clickHandler); if (keydownHandler) document.removeEventListener('keydown', keydownHandler); if (checkStop) clearInterval(checkStop); }; const safeResolve = (val) => { if (!isResolved) { isResolved = true; cleanUp(); resolve(val); } }; clickHandler = (e) => { if (isPaused) return; const target = e.target.closest('span, button, a, div'); if (target && target.innerText.trim() === SELECTORS.saveBtnText) { const textarea = getFollowTextarea(); if (textarea && textarea.value.trim() === '') { showToast('❌ 备注为空!', 'error'); return; } if (textarea && textarea.value.trim() !== '') { const val = textarea.value.trim(); const displayText = val.length > 150 ? val.substring(0, 150) + '...' : val; addLog(`📝 已录入跟进内容: ${displayText}`, 'info', true); } safeResolve(true); } }; document.addEventListener('click', clickHandler); keydownHandler = (e) => { if (e.ctrlKey && e.shiftKey && e.key === 'Enter') { e.preventDefault(); const textarea = getFollowTextarea(); if (document.activeElement === textarea && textarea) { const saveBtn = findClickableBtn(SELECTORS.saveBtnText); if (saveBtn) { saveBtn.click(); } } } if (e.ctrlKey && e.shiftKey && e.key === 'Backspace') { const textarea = getFollowTextarea(); if (document.activeElement === textarea && textarea && textarea.value.length > 0) { e.preventDefault(); textarea.value = ''; textarea.dispatchEvent(new Event('input', { bubbles: true })); showToast('🧹 已清空备注框内容', 'info'); } } }; document.addEventListener('keydown', keydownHandler); checkStop = setInterval(() => { if (stopFlag) { safeResolve(false); } }, 100); }); } // ========================================== // 12. 面板功能绑定与快捷键 // ========================================== startBtn.onclick = () => { if (isRunning) return; if (isKeyboardLocked) { showToast('🔒 键盘已锁定!', 'warn'); return; } if (searchInput) { searchInput.value = ''; if (searchClearBtn) searchClearBtn.style.display = 'none'; renderLogContent(''); } stopFlag = false; isPaused = false; updatePauseButtonState(); isRunning = true; lastProcessedPhone = ''; addLog('🚀 脚本已激活,开始执行自动循环', 'success'); showToast('🚀 开始循环', 'success'); autoProcess(); }; pauseBtn.onclick = () => { if (!isRunning) return; if (isKeyboardLocked) { showToast('🔒 键盘已锁定!', 'warn'); return; } isPaused = !isPaused; updatePauseButtonState(); if (isPaused) { addLog('⏸️ 用户点击了暂停,脚本已挂起', 'info'); showToast('⏸️ 已暂停', 'warn'); } else { addLog('▶️ 用户点击了继续,脚本恢复运行', 'success'); showToast('▶️ 已恢复', 'success'); } }; stopBtn.onclick = () => { if (isKeyboardLocked) { showToast('🔒 键盘已锁定!', 'warn'); return; } stopFlag = true; if (currentTimeout) clearTimeout(currentTimeout); isRunning = false; isPaused = false; updatePauseButtonState(); addLog('⏹️ 用户点击【紧急停止】,脚本已终止', 'error'); showToast('⏹️ 已紧急停止', 'error'); }; clearBtn.onclick = () => { if (isKeyboardLocked) { showToast('🔒 键盘已锁定!', 'warn'); return; } forceRemovePopup(); addLog('🧹 手动点击【强制清除弹窗】,已移除阻挡', 'success', true); showToast('🧹 已强制清除', 'success'); }; hideBtn.onclick = (e) => { e.stopPropagation(); panel.style.display = 'none'; toggleBtn.style.display = 'flex'; updateTogglerStatus(); }; toggleBtn.onclick = (e) => { e.stopPropagation(); panel.style.display = 'flex'; toggleBtn.style.display = 'none'; toggleBtn.classList.remove('toggler-running', 'toggler-paused'); toggleBtn.style.background = '#2196F3'; }; function updateTogglerStatus() { if (toggleBtn.style.display === 'none') return; toggleBtn.classList.remove('toggler-running', 'toggler-paused'); if (isRunning) { if (isPaused) { toggleBtn.style.background = '#FFC107'; toggleBtn.classList.add('toggler-paused'); } else { toggleBtn.style.background = '#2196F3'; toggleBtn.classList.add('toggler-running'); } } else { toggleBtn.style.background = '#2196F3'; } } // 双击回车快速聚焦跟进记录框 let lastEnterTime = 0; document.addEventListener('keydown', (e) => { // 键盘锁定拦截逻辑 if (e.ctrlKey && e.shiftKey && e.key === 'L') { e.preventDefault(); isKeyboardLocked = !isKeyboardLocked; if (isKeyboardLocked) { showToast('🔒 键盘快捷键及面板已锁定 (防误触)', 'warn'); panel.style.opacity = '0.6'; panel.style.filter = 'grayscale(0.5)'; addLog('🔒 用户开启了键盘锁定模式', 'info'); } else { showToast('🔓 键盘快捷键已解锁', 'success'); panel.style.opacity = '1'; panel.style.filter = 'none'; addLog('🔓 用户关闭了键盘锁定模式', 'info'); } return; } if (isKeyboardLocked) { if (e.ctrlKey || e.key === 'Enter') { e.preventDefault(); } return; } if (e.key === 'Enter') { if (!isRunning) return; const targetTextarea = getFollowTextarea(); if (targetTextarea && document.activeElement === targetTextarea) { return; } const now = Date.now(); if (now - lastEnterTime < 300) { e.preventDefault(); if (targetTextarea) { targetTextarea.focus(); showToast('📍 光标已锁定到跟进记录框', 'success'); } } lastEnterTime = now; } if (e.ctrlKey && e.shiftKey) { if (e.key === window._keyStop) { e.preventDefault(); stopBtn.click(); } else if (e.key === window._keyPause) { e.preventDefault(); pauseBtn.click(); } } if (e.ctrlKey && e.altKey) { if (e.key === window._keyScript1) { e.preventDefault(); insertScriptText('script1'); } else if (e.key === window._keyScript2) { e.preventDefault(); insertScriptText('script2'); } else if (e.key === window._keyScript3) { e.preventDefault(); insertScriptText('script3'); } } }); function checkDailyReset() { const currentDateStr = new Date().toDateString(); if (currentDateStr !== lastDate) { logs.length = 0; stats.dialed = 0; stats.saved = 0; isGoalAchieved = false; saveStateToLocal(); updateStatsUI(); if (searchInput) { searchInput.value = ''; if (searchClearBtn) searchClearBtn.style.display = 'none'; } renderLogContent(''); addLog('🔄 新的一天,日志及统计数据已自动清空,祝今天爆单!', 'info'); lastDate = currentDateStr; } } setTimeout(checkDailyReset, 1000); setInterval(checkDailyReset, 60000); console.log('✅ 话术循环切换版加载完成!'); showToast('🔄 快捷话术已支持用 / 分隔实现循环切换', 'success'); })();