1. 号码采集
-1. 输入号码
+拖放文件或点击上传
-支持 TXT, CSV, EXCEL 格式内容提取
- + +点击或拖拽上传文件
+支持 TXT, CSV 等文本格式内容提取
+2. 拆分配置
3. 处理结果
0 个批次请完成上方配置并执行拆分
+等待拆分结果...
diff --git a/assets/css/custom.css b/assets/css/custom.css index 9e119eb..5db627c 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,295 +1,215 @@ :root { - --primary-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); - --accent-gradient: linear-gradient(135deg, #3b82f6 0%, #2dd4bf 100%); - --surface: #ffffff; - --background: #f8fafc; - --border: #e2e8f0; - --text-main: #0f172a; - --text-muted: #64748b; - --radius-lg: 16px; - --radius-md: 12px; - --radius-sm: 8px; - --shadow-subtle: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05); - --shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - --shadow-lg: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --primary-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); + --bg-color: #f8fafc; + --card-bg: rgba(255, 255, 255, 0.8); + --text-main: #1e293b; + --text-muted: #64748b; + --border-color: rgba(226, 232, 240, 0.8); } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; - background-color: var(--background); - color: var(--text-main); - line-height: 1.6; - background-image: - radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%), - radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%), - radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%); - background-attachment: fixed; - background-size: cover; - min-height: 100vh; -} - -/* Glassmorphism background */ -body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(248, 250, 252, 0.85); - backdrop-filter: blur(100px); - z-index: -1; + background-color: var(--bg-color); + background-image: + radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.05) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(168, 85, 247, 0.05) 0px, transparent 50%); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--text-main); + min-height: 100vh; + letter-spacing: -0.01em; } .navbar { - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(255, 255, 255, 0.3); - padding: 1.25rem 0; - position: sticky; - top: 0; - z-index: 1000; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border-color); + padding: 1rem 0; + position: sticky; + top: 0; + z-index: 1000; } .navbar-brand { - font-weight: 800; - font-size: 1.5rem; - background: var(--primary-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - display: flex; - align-items: center; - gap: 0.75rem; -} - -.card { - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.4); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-subtle); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); -} - -.btn { - font-weight: 600; - border-radius: var(--radius-md); - padding: 0.75rem 1.5rem; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.625rem; - border: none; -} - -.btn-primary { - background: var(--primary-gradient); - color: white; - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); -} - -.btn-primary:hover { - transform: scale(1.02); - box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); - background: var(--primary-gradient); - filter: brightness(1.1); -} - -.btn-light { - background: rgba(241, 245, 249, 0.8); - color: var(--text-main); -} - -.form-control, .form-select { - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 0.75rem 1rem; - background: rgba(255, 255, 255, 0.8); - transition: all 0.2s ease; -} - -.form-control:focus { - background: #fff; - border-color: #6366f1; - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); -} - -.dropzone { - border: 2px dashed #cbd5e1; - border-radius: var(--radius-md); - padding: 3.5rem 2rem; - background: rgba(248, 250, 252, 0.5); - cursor: pointer; - transition: all 0.3s ease; -} - -.dropzone:hover { - border-color: #6366f1; - background: rgba(99, 102, 241, 0.05); - transform: translateY(-2px); -} - -.dropzone i { - background: var(--primary-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-size: 3rem; - margin-bottom: 1.5rem; -} - -.stats-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.25rem; - margin-bottom: 2rem; -} - -.stat-item { - background: white; - padding: 1.25rem; - border-radius: var(--radius-md); - border: 1px solid var(--border); - position: relative; - overflow: hidden; -} - -.stat-item::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: var(--primary-gradient); -} - -.stat-label { - font-size: 0.75rem; - font-weight: 700; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 0.5rem; -} - -.stat-value { - font-size: 1.875rem; - font-weight: 800; - color: var(--text-main); - line-height: 1; -} - -.batch-card { - border: none; - background: white; - margin-bottom: 1.25rem; - position: relative; - overflow: hidden; -} - -.batch-card::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 6px; - height: 100%; - background: var(--accent-gradient); -} - -.badge-count { - background: rgba(59, 130, 246, 0.1); - color: #2563eb; - font-weight: 700; - padding: 0.35rem 0.75rem; - border-radius: 9999px; -} - -.batch-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.batch-title { - font-weight: 700; - color: var(--text-main); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.dynamic-batch-input { - animation: fadeIn 0.3s ease-out; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.sticky-panel { - position: sticky; - top: 6rem; -} - -footer { - background: transparent; - margin-top: 4rem; + font-weight: 800; + font-size: 1.5rem; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: flex; + align-items: center; + gap: 0.5rem; } .hero-section { - text-align: center; - padding: 4rem 0 2rem; + padding: 4rem 0 3rem; + text-align: center; } .hero-section h1 { - font-weight: 900; - font-size: 3rem; - letter-spacing: -0.05em; - margin-bottom: 1rem; - background: var(--primary-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + font-weight: 900; + font-size: clamp(2.5rem, 5vw, 4rem); + margin-bottom: 1rem; + letter-spacing: -0.04em; + background: linear-gradient(to bottom, #1e293b, #475569); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } .hero-section p { - color: var(--text-muted); - font-size: 1.25rem; - max-width: 600px; - margin: 0 auto; + color: var(--text-muted); + font-size: 1.25rem; + max-width: 600px; + margin: 0 auto; } -/* Custom Scrollbar */ +.card { + background: var(--card-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--border-color); + border-radius: 24px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.dropzone { + border: 2px dashed #cbd5e1; + border-radius: 20px; + padding: 3rem 2rem; + text-align: center; + transition: all 0.2s ease; + cursor: pointer; + background: rgba(248, 250, 252, 0.5); +} + +.dropzone:hover, .dropzone.drag-active { + border-color: #6366f1; + background: rgba(99, 102, 241, 0.05); + transform: scale(1.01); +} + +.dropzone.drag-active { + border-style: solid; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); +} + +.dropzone i { + width: 48px; + height: 48px; + color: #6366f1; + margin-bottom: 1rem; +} + +.form-control, .form-select { + border-radius: 12px; + padding: 0.75rem 1rem; + border: 2px solid #f1f5f9; + font-weight: 500; +} + +.form-control:focus, .form-select:focus { + border-color: #6366f1; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); +} + +.stats-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.stat-item { + background: white; + padding: 1.25rem; + border-radius: 16px; + border: 1px solid #f1f5f9; +} + +.stat-label { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.stat-value { + font-size: 1.75rem; + font-weight: 800; +} + +.btn-primary { + background: var(--primary-gradient); + border: none; + border-radius: 14px; + font-weight: 700; + padding: 0.8rem 1.5rem; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); + transition: all 0.2s ease; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); +} + +.batch-card { + margin-bottom: 1.5rem; + border-left: 6px solid #6366f1; +} + +.batch-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.batch-title { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.badge-count { + background: #f1f5f9; + color: #475569; + padding: 0.25rem 0.75rem; + border-radius: 99px; + font-size: 0.85rem; + font-weight: 600; +} + +.sticky-panel { + position: sticky; + top: 6rem; +} + +.dynamic-batch-input .input-group-text { + min-width: 100px; + font-size: 0.8rem; + font-weight: 600; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fadeIn { + animation: fadeIn 0.4s ease forwards; +} + +/* Custom scrollbar */ ::-webkit-scrollbar { - width: 10px; + width: 8px; } ::-webkit-scrollbar-track { - background: #f1f5f9; + background: #f1f5f9; } ::-webkit-scrollbar-thumb { - background: #cbd5e1; - border-radius: 5px; + background: #cbd5e1; + border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { - background: #94a3b8; -} - -/* Glass Detail Summary */ -details summary { - padding: 0.5rem 0; - color: var(--accent); - font-weight: 600; - cursor: pointer; - transition: all 0.2s; -} -details summary:hover { - filter: brightness(0.8); -} + background: #94a3b8; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 5fcc34f..87e1ff2 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -16,9 +16,15 @@ document.addEventListener('DOMContentLoaded', () => { let allNumbers = []; - // Helper: Extract numbers from text (Mobile number regex) + // Helper: Extract numbers from text (Flexible digit extraction) const extractNumbers = (text) => { - const matches = text.match(/1[3-9]\d{9}/g) || []; + // First try to match standard 11-digit mobile numbers + let matches = text.match(/1[3-9]\d{9}/g) || []; + + // If no mobile numbers found, try matching any sequence of 5-15 digits (IDs, account numbers, etc.) + if (matches.length === 0) { + matches = text.match(/\d{5,15}/g) || []; + } return matches; }; @@ -42,7 +48,6 @@ document.addEventListener('DOMContentLoaded', () => { const mode = splitMode.value; if (mode === 'count') { unitText.textContent = '份'; - dynamicCountsContainer.classList.remove('d-none'); generateDynamicInputs(); } else { unitText.textContent = '条'; @@ -54,7 +59,7 @@ document.addEventListener('DOMContentLoaded', () => { const numBatches = parseInt(splitValue.value); dynamicInputsList.innerHTML = ''; - if (isNaN(numBatches) || numBatches <= 1) { + if (isNaN(numBatches) || numBatches <= 1 || splitMode.value !== 'count') { dynamicCountsContainer.classList.add('d-none'); return; } @@ -64,10 +69,10 @@ document.addEventListener('DOMContentLoaded', () => { // Create N-1 inputs because the last one is always the remainder for (let i = 0; i < numBatches - 1; i++) { const div = document.createElement('div'); - div.className = 'input-group input-group-sm dynamic-batch-input'; + div.className = 'input-group input-group-sm mb-2'; div.innerHTML = ` - 第 ${i + 1} 份数量 - + 第 ${i + 1} 份数量 + `; dynamicInputsList.appendChild(div); } @@ -77,29 +82,46 @@ document.addEventListener('DOMContentLoaded', () => { splitValue.addEventListener('input', generateDynamicInputs); // File handling - dropzone.addEventListener('click', () => fileInput.click()); - fileInput.addEventListener('change', async (e) => { - const file = e.target.files[0]; + const handleFile = (file) => { if (!file) return; - - if (file.name.endsWith('.txt') || file.name.endsWith('.csv')) { - const reader = new FileReader(); - reader.onload = (event) => { - const content = event.target.result; - numberInput.value += (numberInput.value ? '\n' : '') + content; - updateStats(); - fileInput.value = ''; - }; - reader.readAsText(file); - } else { - // Placeholder for Excel/Word/PDF if needed, but for now just inform - alert('当前支持直接读取 .txt 和 .csv 文件。Word/Excel/PDF 请直接复制其中的号码并粘贴到输入框。'); - } + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target.result; + numberInput.value += (numberInput.value ? '\n' : '') + content; + updateStats(); + fileInput.value = ''; + }; + reader.readAsText(file); + }; + + dropzone.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', (e) => handleFile(e.target.files[0])); + + // Drag and Drop + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, (e) => { + e.preventDefault(); + e.stopPropagation(); + }, false); }); + ['dragenter', 'dragover'].forEach(eventName => { + dropzone.addEventListener(eventName, () => dropzone.classList.add('drag-active'), false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, () => dropzone.classList.remove('drag-active'), false); + }); + + dropzone.addEventListener('drop', (e) => { + const dt = e.dataTransfer; + const file = dt.files[0]; + handleFile(file); + }, false); + // Splitting logic splitBtn.addEventListener('click', () => { - updateStats(); // Ensure we have latest data + updateStats(); if (allNumbers.length === 0) { alert('请先导入或粘贴号码!'); return; @@ -118,56 +140,47 @@ document.addEventListener('DOMContentLoaded', () => { if (mode === 'count') { const sizeInputs = document.querySelectorAll('.batch-size-input'); - let hasSpecificSizes = false; + let hasAnySpecificSize = false; let specificSizes = []; sizeInputs.forEach(input => { const s = parseInt(input.value); if (!isNaN(s) && s > 0) { - hasSpecificSizes = true; + hasAnySpecificSize = true; specificSizes.push(s); } else { specificSizes.push(null); } }); - if (hasSpecificSizes) { - // Specific size splitting + if (hasAnySpecificSize) { + // If some sizes are specified, follow them. for (let i = 0; i < val - 1; i++) { const size = specificSizes[i]; if (size !== null && size > 0) { batches.push(tempNumbers.splice(0, size)); } else { - // If one is empty but others are not, we need a strategy. - // Let's treat empty as 0 for now or average the rest? - // User said "if no number, system default split". - // But if some have numbers and others don't, it's ambiguous. - // We'll calculate the remaining needed batches. - const remainingCount = val - batches.length; - const avg = Math.ceil(tempNumbers.length / remainingCount); + // For empty inputs when others are filled, we calculate an average of the remaining + const remainingSlots = val - batches.length; + const avg = Math.ceil(tempNumbers.length / remainingSlots); batches.push(tempNumbers.splice(0, avg)); } } - // Add the remainder to the last batch - if (tempNumbers.length > 0) { - batches.push(tempNumbers); - } else if (batches.length < val) { - // If no numbers left but we need more batches, add empty ones - while (batches.length < val) batches.push([]); - } + // Last batch always gets the remainder + batches.push(tempNumbers); } else { - // Average splitting + // Default even split if no inputs are filled const batchSize = Math.ceil(tempNumbers.length / val); for (let i = 0; i < val; i++) { - if (tempNumbers.length > 0) { - batches.push(tempNumbers.splice(0, batchSize)); + if (i === val - 1) { + batches.push(tempNumbers); // Last one takes all remainder } else { - batches.push([]); + batches.push(tempNumbers.splice(0, batchSize)); } } } } else { - // Split by fixed size per batch + // Split by fixed size per batch (mode === 'size') while (tempNumbers.length > 0) { batches.push(tempNumbers.splice(0, val)); } @@ -178,39 +191,36 @@ document.addEventListener('DOMContentLoaded', () => { const renderResults = (batches) => { resultsContainer.innerHTML = ''; - batchCountBadge.textContent = `${batches.length} 个批次`; + // Filter out empty batches if they occurred due to split logic + const finalBatches = batches.filter(b => b.length > 0); + + batchCountBadge.textContent = `${finalBatches.length} 个批次`; batchCountBadge.classList.remove('d-none'); - batches.forEach((batch, index) => { + finalBatches.forEach((batch, index) => { const card = document.createElement('div'); card.className = 'card batch-card animate-fadeIn'; - const previewText = batch.slice(0, 30).join(', '); - const hasMore = batch.length > 30; + const previewText = batch.slice(0, 15).join(', '); + const hasMore = batch.length > 15; card.innerHTML = `
支持万级号码导入、全自动去重、自定义灵活分批,提升您的工作效率。
+支持万级号码一键去重、灵活分批。专注效率,保护隐私。
支持 TXT, CSV, EXCEL 格式内容提取
- + +支持 TXT, CSV 等文本格式内容提取
+请完成上方配置并执行拆分
+等待拆分结果...