// ==UserScript== // @name 网页字数统计器 1.0 Pro // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 字数统计 | 字符统计支持去空格/不去空格 | 可拖动设置面板 // @author 拾壹有点呆,本工具由Chat-GPT,DeepSeek,Gemini,Dobao完成 // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; /* ========== 全局变量 ========== */ let currentMode = '字符'; // 统计模式:字符 / 中文 / 英文单词 let charCountMode = 'trim'; // 字符统计子选项:trim(去空格) / keep(不去空格) let isCounterHidden = false; let dragStarted = false; let dragPerformed = false; let dragOffsetX = 0, dragOffsetY = 0; let mouseDownX = 0, mouseDownY = 0; let suppressOpen = false; let counterBox = null; let settingsPanel = null; /* ========== 创建 UI ========== */ const style = document.createElement('style'); style.innerHTML = ` #word-counter-pro { position: fixed; bottom: 20px; right: 20px; padding: 10px 14px; background: rgba(0, 123, 255, 0.9); color: #fff; border-radius: 10px; font-size: 13px; font-family: sans-serif; z-index: 999999999; box-shadow: 0 4px 12px rgba(0,0,0,0.3); cursor: move; user-select: none; transition: background 0.2s, font-size 0.2s, padding 0.2s; } .wc-settings-panel { position: fixed; background: #fff; color: #333; border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.2); padding: 0; font-size: 14px; font-family: system-ui, 'Segoe UI', sans-serif; z-index: 1000000000; width: 280px; backdrop-filter: blur(2px); border: 1px solid rgba(0,0,0,0.1); cursor: default; overflow: hidden; } .wc-settings-header { background: #f8f9fa; padding: 12px 16px; cursor: move; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center; user-select: none; } .wc-settings-header h4 { margin: 0; font-size: 16px; color: #007bff; } .wc-settings-header .close-icon { cursor: pointer; font-size: 18px; line-height: 1; padding: 0 4px; color: #888; } .wc-settings-header .close-icon:hover { color: #333; } .wc-settings-content { padding: 14px 18px; } .setting-row { margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; gap: 12px; } .setting-row label { font-weight: 500; width: 70px; } .setting-row select, .setting-row input[type="color"], .setting-row input[type="range"] { flex: 1; padding: 5px; border-radius: 6px; border: 1px solid #ccc; } .color-row { display: flex; gap: 8px; align-items: center; flex: 1; } .color-row input { flex: 1; } .radio-group { display: flex; gap: 12px; align-items: center; } .radio-group label { width: auto; font-weight: normal; display: flex; align-items: center; gap: 4px; cursor: pointer; } button { width: 100%; padding: 6px 0; margin-top: 8px; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: 0.2s; } .btn-hide-counter { background: #dc3545; color: white; } .btn-hide-counter:hover { background: #c82333; } .value-display { min-width: 40px; text-align: right; font-size: 12px; color: #666; } .sub-setting { margin-left: 10px; padding-left: 10px; border-left: 2px solid #ddd; } `; document.head.appendChild(style); counterBox = document.createElement('div'); counterBox.id = 'word-counter-pro'; counterBox.innerText = '统计中...'; document.body.appendChild(counterBox); settingsPanel = document.createElement('div'); settingsPanel.className = 'wc-settings-panel'; settingsPanel.style.display = 'none'; document.body.appendChild(settingsPanel); /* ========== 辅助函数 ========== */ function getText() { const clone = document.body.cloneNode(true); clone.querySelectorAll('script, style, nav, footer, noscript, meta, link').forEach(el => el.remove()); return clone.textContent || ''; } function countText(text) { if (currentMode === '字符') { if (charCountMode === 'trim') { return text.replace(/\s+/g, '').length; } else { return text.length; // 不去空格,包括空格、换行等 } } if (currentMode === '中文') { return (text.match(/[\u4e00-\u9fa5]/g) || []).length; } if (currentMode === '英文单词') { return (text.match(/\b[a-zA-Z]+\b/g) || []).length; } return 0; } function updateCount() { if (isCounterHidden) return; const text = getText(); const count = countText(text); counterBox.innerText = `${currentMode}: ${count}`; } let updateTimer = null; function scheduleUpdate() { if (updateTimer) return; updateTimer = setTimeout(() => { updateCount(); updateTimer = null; }, 500); } function applyCustomStyle(colorHex, alpha, fontSize, padding) { let r = 0, g = 123, b = 255; if (colorHex && colorHex.startsWith('#')) { r = parseInt(colorHex.slice(1,3), 16); g = parseInt(colorHex.slice(3,5), 16); b = parseInt(colorHex.slice(5,7), 16); } const bgColor = `rgba(${r}, ${g}, ${b}, ${alpha})`; counterBox.style.backgroundColor = bgColor; counterBox.style.fontSize = fontSize + 'px'; counterBox.style.padding = padding + 'px ' + (padding + 4) + 'px'; } function syncControlsToPanel() { const bgColor = counterBox.style.backgroundColor; let alpha = 0.9, r = 0, g = 123, b = 255; const match = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); if (match) { r = parseInt(match[1]); g = parseInt(match[2]); b = parseInt(match[3]); alpha = match[4] ? parseFloat(match[4]) : 1; } const hex = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); const colorInput = document.getElementById('wc-bg-color'); if (colorInput) colorInput.value = hex; const alphaSlider = document.getElementById('wc-alpha'); if (alphaSlider) { alphaSlider.value = alpha; const alphaVal = document.getElementById('wc-alpha-val'); if (alphaVal) alphaVal.innerText = alpha.toFixed(2); } let fontSize = parseInt(counterBox.style.fontSize); if (isNaN(fontSize)) fontSize = 13; const fontSizeSlider = document.getElementById('wc-font-size'); if (fontSizeSlider) { fontSizeSlider.value = fontSize; const fontSizeVal = document.getElementById('wc-font-size-val'); if (fontSizeVal) fontSizeVal.innerText = fontSize + 'px'; } let paddingVal = parseInt(counterBox.style.padding); if (isNaN(paddingVal)) paddingVal = 10; const paddingSlider = document.getElementById('wc-padding'); if (paddingSlider) { paddingSlider.value = paddingVal; const paddingValSpan = document.getElementById('wc-padding-val'); if (paddingValSpan) paddingValSpan.innerText = paddingVal + 'px'; } } // 设置面板拖动 let panelDragActive = false; let panelStartX = 0, panelStartY = 0; let panelInitialLeft = 0, panelInitialTop = 0; function initPanelDrag(headerElement) { if (!headerElement) return; headerElement.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); panelDragActive = true; panelStartX = e.clientX; panelStartY = e.clientY; const rect = settingsPanel.getBoundingClientRect(); panelInitialLeft = rect.left; panelInitialTop = rect.top; settingsPanel.style.cursor = 'grabbing'; }); } function handlePanelDragMove(e) { if (!panelDragActive) return; let dx = e.clientX - panelStartX; let dy = e.clientY - panelStartY; let newLeft = panelInitialLeft + dx; let newTop = panelInitialTop + dy; newLeft = Math.min(Math.max(newLeft, 0), window.innerWidth - settingsPanel.offsetWidth); newTop = Math.min(Math.max(newTop, 0), window.innerHeight - settingsPanel.offsetHeight); settingsPanel.style.left = newLeft + 'px'; settingsPanel.style.top = newTop + 'px'; settingsPanel.style.right = 'auto'; settingsPanel.style.bottom = 'auto'; } function stopPanelDrag() { panelDragActive = false; settingsPanel.style.cursor = ''; } function buildSettingsPanel() { settingsPanel.innerHTML = `

⚙️ 计数器设置

0.90
13px
10px
`; const dragHandle = document.getElementById('wc-drag-handle'); initPanelDrag(dragHandle); document.getElementById('wc-panel-close').addEventListener('click', (e) => { e.stopPropagation(); hideSettingsPanel(); }); // 统计模式切换 const modeSelect = document.getElementById('wc-mode-select'); modeSelect.value = currentMode; const charSubDiv = document.getElementById('char-sub-settings'); function toggleCharSubSettings() { if (modeSelect.value === '字符') { charSubDiv.style.display = 'block'; } else { charSubDiv.style.display = 'none'; } } toggleCharSubSettings(); modeSelect.addEventListener('change', (e) => { currentMode = e.target.value; toggleCharSubSettings(); updateCount(); }); // 字符统计子选项 const radioTrim = document.querySelector('input[name="charMode"][value="trim"]'); const radioKeep = document.querySelector('input[name="charMode"][value="keep"]'); if (charCountMode === 'trim') radioTrim.checked = true; else radioKeep.checked = true; radioTrim.addEventListener('change', () => { if (radioTrim.checked) { charCountMode = 'trim'; updateCount(); } }); radioKeep.addEventListener('change', () => { if (radioKeep.checked) { charCountMode = 'keep'; updateCount(); } }); // 样式控件 const colorPicker = document.getElementById('wc-bg-color'); const alphaSlider = document.getElementById('wc-alpha'); const alphaVal = document.getElementById('wc-alpha-val'); const fontSizeSlider = document.getElementById('wc-font-size'); const fontSizeVal = document.getElementById('wc-font-size-val'); const paddingSlider = document.getElementById('wc-padding'); const paddingVal = document.getElementById('wc-padding-val'); function updateStyleFromControls() { const color = colorPicker.value; const alpha = parseFloat(alphaSlider.value); const fontSize = parseInt(fontSizeSlider.value); const padding = parseInt(paddingSlider.value); applyCustomStyle(color, alpha, fontSize, padding); } colorPicker.addEventListener('input', updateStyleFromControls); alphaSlider.addEventListener('input', () => { alphaVal.innerText = parseFloat(alphaSlider.value).toFixed(2); updateStyleFromControls(); }); fontSizeSlider.addEventListener('input', () => { fontSizeVal.innerText = fontSizeSlider.value + 'px'; updateStyleFromControls(); }); paddingSlider.addEventListener('input', () => { paddingVal.innerText = paddingSlider.value + 'px'; updateStyleFromControls(); }); document.getElementById('wc-hide-counter').addEventListener('click', () => { isCounterHidden = true; counterBox.style.display = 'none'; hideSettingsPanel(); }); } function showSettingsPanel() { if (isCounterHidden) return; buildSettingsPanel(); syncControlsToPanel(); settingsPanel.style.display = 'block'; settingsPanel.style.left = '-9999px'; settingsPanel.style.top = '-9999px'; const panelRect = settingsPanel.getBoundingClientRect(); const panelWidth = panelRect.width; const panelHeight = panelRect.height; const counterRect = counterBox.getBoundingClientRect(); let left = counterRect.right + 10; let top = counterRect.top; if (left + panelWidth > window.innerWidth) { left = counterRect.left - panelWidth - 10; if (left < 0) left = 10; } if (top + panelHeight > window.innerHeight) { top = window.innerHeight - panelHeight - 10; } if (top < 0) top = 10; settingsPanel.style.left = left + 'px'; settingsPanel.style.top = top + 'px'; settingsPanel.style.right = 'auto'; settingsPanel.style.bottom = 'auto'; } function hideSettingsPanel() { settingsPanel.style.display = 'none'; panelDragActive = false; } function handleOutsideClick(e) { if (!settingsPanel || settingsPanel.style.display !== 'block') return; if (!counterBox.contains(e.target) && !settingsPanel.contains(e.target)) { hideSettingsPanel(); } } /* ========== 悬浮窗交互 ========== */ counterBox.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); if (settingsPanel.style.display === 'block') { hideSettingsPanel(); suppressOpen = true; } dragStarted = true; dragPerformed = false; mouseDownX = e.clientX; mouseDownY = e.clientY; const rect = counterBox.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; counterBox.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!dragStarted) return; const dx = Math.abs(e.clientX - mouseDownX); const dy = Math.abs(e.clientY - mouseDownY); if (dx > 5 || dy > 5) dragPerformed = true; if (dragPerformed) { let left = e.clientX - dragOffsetX; let top = e.clientY - dragOffsetY; left = Math.min(Math.max(left, 0), window.innerWidth - counterBox.offsetWidth); top = Math.min(Math.max(top, 0), window.innerHeight - counterBox.offsetHeight); counterBox.style.left = left + 'px'; counterBox.style.top = top + 'px'; counterBox.style.right = 'auto'; counterBox.style.bottom = 'auto'; } }); document.addEventListener('mouseup', () => { if (dragStarted && !dragPerformed && !suppressOpen) { if (!isCounterHidden) { if (settingsPanel.style.display === 'block') { hideSettingsPanel(); } else { showSettingsPanel(); } } } dragStarted = false; dragPerformed = false; suppressOpen = false; counterBox.style.cursor = 'move'; }); document.addEventListener('mousemove', handlePanelDragMove); document.addEventListener('mouseup', stopPanelDrag); /* ========== 内容变化监听 ========== */ const observer = new MutationObserver(scheduleUpdate); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); window.addEventListener('input', scheduleUpdate); document.addEventListener('click', handleOutsideClick); /* ========== 初始化 ========== */ function init() { isCounterHidden = false; counterBox.style.display = 'block'; currentMode = '字符'; charCountMode = 'trim'; // 默认去空格 applyCustomStyle('#007bff', 0.9, 13, 10); updateCount(); } init(); })();