247 lines
10 KiB
JavaScript
247 lines
10 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const numberInput = document.getElementById('numberInput');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const dropzone = document.getElementById('dropzone');
|
|
const splitMode = document.getElementById('splitMode');
|
|
const splitValue = document.getElementById('splitValue');
|
|
const splitBtn = document.getElementById('splitBtn');
|
|
const autoDedupe = document.getElementById('autoDedupe');
|
|
const resultsContainer = document.getElementById('resultsContainer');
|
|
const totalCountEl = document.getElementById('totalCount');
|
|
const uniqueCountEl = document.getElementById('uniqueCount');
|
|
const dynamicCountsContainer = document.getElementById('dynamicCountsContainer');
|
|
const dynamicInputsList = document.getElementById('dynamicInputsList');
|
|
const unitText = document.getElementById('unitText');
|
|
const batchCountBadge = document.getElementById('batchCountBadge');
|
|
|
|
let allNumbers = [];
|
|
|
|
// Helper: Extract numbers from text (Mobile number regex)
|
|
const extractNumbers = (text) => {
|
|
const matches = text.match(/1[3-9]\d{9}/g) || [];
|
|
return matches;
|
|
};
|
|
|
|
// Update stats and allNumbers array
|
|
const updateStats = () => {
|
|
const text = numberInput.value;
|
|
const extracted = extractNumbers(text);
|
|
const unique = [...new Set(extracted)];
|
|
|
|
totalCountEl.textContent = extracted.length.toLocaleString();
|
|
uniqueCountEl.textContent = unique.length.toLocaleString();
|
|
|
|
allNumbers = autoDedupe.checked ? unique : extracted;
|
|
};
|
|
|
|
numberInput.addEventListener('input', updateStats);
|
|
autoDedupe.addEventListener('change', updateStats);
|
|
|
|
// Dynamic UI for split modes
|
|
const updateSplitUI = () => {
|
|
const mode = splitMode.value;
|
|
if (mode === 'count') {
|
|
unitText.textContent = '份';
|
|
dynamicCountsContainer.classList.remove('d-none');
|
|
generateDynamicInputs();
|
|
} else {
|
|
unitText.textContent = '条';
|
|
dynamicCountsContainer.classList.add('d-none');
|
|
}
|
|
};
|
|
|
|
const generateDynamicInputs = () => {
|
|
const numBatches = parseInt(splitValue.value);
|
|
dynamicInputsList.innerHTML = '';
|
|
|
|
if (isNaN(numBatches) || numBatches <= 1) {
|
|
dynamicCountsContainer.classList.add('d-none');
|
|
return;
|
|
}
|
|
|
|
dynamicCountsContainer.classList.remove('d-none');
|
|
|
|
// 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.innerHTML = `
|
|
<span class="input-group-text bg-white border-2">第 ${i + 1} 份数量</span>
|
|
<input type="number" class="form-control border-2 batch-size-input" placeholder="留空则自动平均分配" min="1">
|
|
`;
|
|
dynamicInputsList.appendChild(div);
|
|
}
|
|
};
|
|
|
|
splitMode.addEventListener('change', updateSplitUI);
|
|
splitValue.addEventListener('input', generateDynamicInputs);
|
|
|
|
// File handling
|
|
dropzone.addEventListener('click', () => fileInput.click());
|
|
fileInput.addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
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 请直接复制其中的号码并粘贴到输入框。');
|
|
}
|
|
});
|
|
|
|
// Splitting logic
|
|
splitBtn.addEventListener('click', () => {
|
|
updateStats(); // Ensure we have latest data
|
|
if (allNumbers.length === 0) {
|
|
alert('请先导入或粘贴号码!');
|
|
return;
|
|
}
|
|
|
|
const mode = splitMode.value;
|
|
const val = parseInt(splitValue.value);
|
|
|
|
if (isNaN(val) || val <= 0) {
|
|
alert('请输入有效的分批数值!');
|
|
return;
|
|
}
|
|
|
|
let batches = [];
|
|
let tempNumbers = [...allNumbers];
|
|
|
|
if (mode === 'count') {
|
|
const sizeInputs = document.querySelectorAll('.batch-size-input');
|
|
let hasSpecificSizes = false;
|
|
let specificSizes = [];
|
|
|
|
sizeInputs.forEach(input => {
|
|
const s = parseInt(input.value);
|
|
if (!isNaN(s) && s > 0) {
|
|
hasSpecificSizes = true;
|
|
specificSizes.push(s);
|
|
} else {
|
|
specificSizes.push(null);
|
|
}
|
|
});
|
|
|
|
if (hasSpecificSizes) {
|
|
// Specific size splitting
|
|
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);
|
|
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([]);
|
|
}
|
|
} else {
|
|
// Average splitting
|
|
const batchSize = Math.ceil(tempNumbers.length / val);
|
|
for (let i = 0; i < val; i++) {
|
|
if (tempNumbers.length > 0) {
|
|
batches.push(tempNumbers.splice(0, batchSize));
|
|
} else {
|
|
batches.push([]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Split by fixed size per batch
|
|
while (tempNumbers.length > 0) {
|
|
batches.push(tempNumbers.splice(0, val));
|
|
}
|
|
}
|
|
|
|
renderResults(batches);
|
|
});
|
|
|
|
const renderResults = (batches) => {
|
|
resultsContainer.innerHTML = '';
|
|
batchCountBadge.textContent = `${batches.length} 个批次`;
|
|
batchCountBadge.classList.remove('d-none');
|
|
|
|
batches.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;
|
|
|
|
card.innerHTML = `
|
|
<div class="card-body p-4">
|
|
<div class="batch-header">
|
|
<div class="batch-title">
|
|
<span class="badge bg-primary">第 ${index + 1} 批</span>
|
|
<span class="badge-count">${batch.length.toLocaleString()} 条号码</span>
|
|
</div>
|
|
<button class="btn btn-primary btn-sm px-3 download-btn" data-index="${index}">
|
|
<i data-lucide="download" style="width: 14px;"></i> 下载 TXT
|
|
</button>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text bg-light border-0"><i data-lucide="tag" style="width: 12px;"></i></span>
|
|
<input type="text" class="form-control border-0 bg-light remark-input" placeholder="输入备注(将作为文件名)...">
|
|
</div>
|
|
</div>
|
|
<details>
|
|
<summary class="small">查看部分预览</summary>
|
|
<div class="mt-2 p-3 bg-light rounded-3 small text-muted font-monospace" style="max-height: 120px; overflow-y: auto; border: 1px solid rgba(0,0,0,0.05);">
|
|
${batch.length > 0 ? previewText + (hasMore ? ' ...' : '') : '<span class="fst-italic">此批次为空</span>'}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
`;
|
|
|
|
resultsContainer.appendChild(card);
|
|
|
|
// Download handler
|
|
card.querySelector('.download-btn').addEventListener('click', () => {
|
|
if (batch.length === 0) {
|
|
alert('此批次没有号码可下载。');
|
|
return;
|
|
}
|
|
const remark = card.querySelector('.remark-input').value.trim() || `批次_${index + 1}_${batch.length}条`;
|
|
const blob = new Blob([batch.join('\n')], { type: 'text/plain;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${remark}.txt`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
});
|
|
|
|
if (window.lucide) {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
|
|
// Initial UI Setup
|
|
updateSplitUI();
|
|
}); |