// ==UserScript==
// @name 终极CRM半自动外呼循环(六大功能集成版)
// @namespace http://tampermonkey.net/
// @version 12.21
// @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,
autoNext: true, // 🆕 功能6: 自动进入下一位
hoverTransparent: true, // 🆕 功能4: 悬停半透明
lockStartOnGoal: true, // 🆕 功能3: 达标锁定开始按钮
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 };
// 🆕 状态变量
let isWaitingForSave = false;
let startLocked = false;
let isGoalAchieved = false;
let isKeyboardLocked = false;
let panelHideTimer = null;
function loadSettings() {
const saved = localStorage.getItem('crm_advanced_settings');
if (saved) {
try {
const parsed = JSON.parse(saved);
settings = deepMerge(settings, parsed);
// 强制恢复锁定状态标志
if (parsed.startLocked) startLocked = parsed.startLocked;
if (parsed.isGoalAchieved) isGoalAchieved = parsed.isGoalAchieved;
} catch (e) {}
}
window._CRMSettings = settings;
updateKeybindsFromSettings();
}
function saveSettings() {
// 将锁定状态一并存入缓存
const state = { ...settings, startLocked, isGoalAchieved };
localStorage.setItem('crm_advanced_settings', JSON.stringify(state));
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 = '';
// ==========================================
// 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.21 | 六大隐藏功能全面集成
🚀 快速启动
- 打开 CRM 客户详情页,点击右下角面板的 【▶ 开始循环】。
- 脚本会自动拨号、检测挂断、等待您录入备注、自动点击【下一位】。
⌨️ 核心快捷键(全键盘操作)
- Ctrl + Shift + S:紧急停止
- Ctrl + Shift + P:暂停 / 继续
- Ctrl + Shift + Enter:一键保存(光标在备注框内时)
- Ctrl + Shift + Backspace:一键清空备注框
- Ctrl + Shift + L:🔒 锁定 / 解锁 键盘快捷键(防误触)
- Ctrl + Shift + Z:🔄 尝试撤销并返回上一客户
- Ctrl + Alt + 1/2/3:循环切换话术A/B/C
- 双击 Enter (连按两次回车):快速将光标锁定至跟进记录输入框
📊 辅助工具
- 【📋 日志】窗口:记录完整操作链路,支持 搜索、清空、导出 为 txt 文件。
- 【📊 战绩】按钮:一键查看今日已拨号、已保存数量及完成率。
- 【⚡ 强制清除弹窗】:页面若卡住或灰屏时,点击此按钮强制解锁。
- 「—」按钮:隐藏面板,变成带实时进度与状态识别的呼吸灯。
- 面板悬浮穿透:鼠标移开面板2秒后自动半透明,不遮挡底层数据。
- 自动翻页开关:可在【设置】中灵活开启/关闭【保存后自动下一位】。
`;
// ==========================================
// 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 = `
⚙️ 全局设置
今日目标拨号数:
${startLocked ? `
🔒 已达到目标,【开始循环】被锁定
` : ''}
`;
modal.appendChild(box);
document.body.appendChild(modal);
// 🆕 绑定强制解锁按钮
const unlockBtn = document.getElementById('sett_unlock_btn');
if (unlockBtn) {
unlockBtn.onclick = () => {
startLocked = false;
isGoalAchieved = false;
showToast('🔓 已强制解锁【开始循环】按钮', 'success');
// 刷新UI
const goalDiv = unlockBtn.closest('div');
if (goalDiv) {
goalDiv.innerHTML = `今日目标拨号数:
`;
}
};
}
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.autoNext = document.getElementById('sett_auto_next').checked;
settings.lockStartOnGoal = document.getElementById('sett_lock_start').checked;
settings.hoverTransparent = document.getElementById('sett_hover_trans').checked;
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();
applyPanelFade(); // 应用悬停半透明
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;
transition: opacity 0.4s ease;
`;
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.id = 'crm-toggle-btn';
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);
// 🆕【功能4】鼠标移出面板2秒后自动半透明
function applyPanelFade() {
if (settings.hoverTransparent) {
panel.style.transition = 'opacity 0.5s ease';
panel.addEventListener('mouseenter', () => {
if (panelHideTimer) clearTimeout(panelHideTimer);
panel.style.opacity = '1';
});
panel.addEventListener('mouseleave', () => {
panelHideTimer = setTimeout(() => {
if (!isDragging) panel.style.opacity = '0.3';
}, 2000);
});
} else {
panel.style.opacity = '1';
panel.removeEventListener('mouseenter', () => {});
panel.removeEventListener('mouseleave', () => {});
}
}
applyPanelFade();
// 呼吸灯 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); }
}
@keyframes breath-waiting {
0% { box-shadow: 0 4px 12px rgba(156,39,176,0.2); transform: scale(1); }
50% { box-shadow: 0 8px 24px rgba(156,39,176,0.6); transform: scale(1.05); }
100% { box-shadow: 0 4px 12px rgba(156,39,176,0.2); transform: scale(1); }
}
.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; }
.toggler-waiting { animation: breath-waiting 3s infinite ease-in-out !important; background: #9C27B0 !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}
`;
// 🆕【功能3】目标达成后锁定开始按钮
if (settings.lockStartOnGoal && settings.dailyGoal > 0 && done >= settings.dailyGoal && !startLocked) {
startLocked = true;
isGoalAchieved = true;
saveSettings();
showToast('🔒 今日目标已达成,为防止误触,【开始循环】按钮已锁定。可在设置中解锁。', 'warn', 5000);
addLog('🔒 目标达成,开始循环按钮已自动锁定', 'info');
}
// 🆕【功能1】更新悬浮球实时进度
if (toggleBtn) {
if (stats.dialed > 0) {
toggleBtn.innerText = `📞 ${stats.dialed}/${settings.dailyGoal}`;
} else {
toggleBtn.innerText = '📞 面板';
}
}
}
}
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) {
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. 核心自动化流程(集成功能6)
// ==========================================
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();
// 🆕【功能6】如果用户关闭了自动翻页,只保存数据,不执行翻页动作
if (settings.autoNext) {
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; }
} else {
addLog('ℹ️ 自动翻页已关闭,请手动点击页面上的【下一位】继续', 'info');
updateStatus('⏸️ 自动翻页关闭,请手动点击【下一位】', '#2196F3');
// 等待手动翻页触发
const manualNext = await waitForManualNext();
if (!manualNext || stopFlag) { isRunning = false; return; }
}
await sleep(1000);
autoProcess();
}
// ==========================================
// 11. 监听保存与快捷键(集成功能2、5)
// ==========================================
function waitForSave() {
return new Promise((resolve) => {
let isResolved = false;
let clickHandler, keydownHandler, checkStop;
// 🆕【功能2】进入等待保存状态
isWaitingForSave = true;
updateTogglerStatus();
const cleanUp = () => {
if (clickHandler) document.removeEventListener('click', clickHandler);
if (keydownHandler) document.removeEventListener('keydown', keydownHandler);
if (checkStop) clearInterval(checkStop);
// 🆕【功能2】退出等待保存状态
isWaitingForSave = false;
updateTogglerStatus();
};
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);
});
}
// 🆕【功能6】手动翻页监听器
function waitForManualNext() {
return new Promise((resolve) => {
let isResolved = false;
let clickHandler, checkStop;
const cleanUp = () => {
if (clickHandler) document.removeEventListener('click', clickHandler);
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.nextBtnText) {
addLog('➡️ 检测到用户手动点击【下一位】', 'success');
safeResolve(true);
}
};
document.addEventListener('click', clickHandler);
checkStop = setInterval(() => {
if (stopFlag) { safeResolve(false); }
}, 100);
});
}
// ==========================================
// 12. 面板功能绑定与快捷键
// ==========================================
startBtn.onclick = () => {
if (isRunning) return;
if (isKeyboardLocked) { showToast('🔒 键盘已锁定!', 'warn'); return; }
if (startLocked) {
showToast('🔒 已达今日目标,按钮已锁定。请在【设置】中点击【强制解锁】。', 'error'); 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', 'toggler-waiting');
toggleBtn.style.background = '#2196F3';
};
function updateTogglerStatus() {
if (toggleBtn.style.display === 'none') return;
toggleBtn.classList.remove('toggler-running', 'toggler-paused', 'toggler-waiting');
if (isRunning) {
if (isPaused) {
toggleBtn.style.background = '#FFC107';
toggleBtn.classList.add('toggler-paused');
} else if (isWaitingForSave) {
// 🆕【功能2】等待保存状态,变紫色呼吸灯
toggleBtn.style.background = '#9C27B0';
toggleBtn.classList.add('toggler-waiting');
} 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;
}
// 🆕【功能5】快捷键 撤销/回退
if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
e.preventDefault();
if (isRunning) {
showToast('🔄 尝试撤销并返回上一客户...', 'info');
addLog('🔄 用户触发了撤销/回退操作', 'info');
window.history.back(); // 尝试浏览器回退
} else {
showToast('⚠️ 脚本未运行,无法撤销', 'error');
}
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;
startLocked = false; // 跨天自动重置锁定
saveSettings();
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('⚡ 6大集成功能:进度球、状态灯、锁定保护、透明面板、撤销回退、翻页开关', 'success');
})();