commit 2ff09f91b0ff126dd1a56da5465a253d82eded1e
Author: aaron <>
Date: Sat Jan 24 22:22:11 2026 +0800
first commit
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b80c25f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,142 @@
+# BSC批量转账工具
+
+一个现代化、简洁、美观的BSC链批量转账工具,支持BNB和BEP-20代币的批量转账。
+
+## ✨ 界面特色
+
+- 🎨 **现代化设计** - 采用BSC品牌金黄色系,简洁大方
+- 🌈 **优雅配色** - 浅色主题,护眼舒适
+- 📱 **响应式布局** - 完美适配桌面和移动设备
+- ⚡ **流畅动画** - 微交互动画,提升用户体验
+- 🎯 **清晰层次** - 卡片式设计,信息层次分明
+
+## 功能特性
+
+✅ 支持BNB原生代币批量转账
+✅ 支持BEP-20代币批量转账
+✅ 文本输入批量数据
+✅ 实时转账进度显示
+✅ 转账历史记录查看
+✅ 交易记录导出
+✅ Gas费用预估
+
+## 运行方法
+
+### 方式1:直接打开
+```bash
+open index.html
+```
+
+### 方式2:本地服务器(推荐)
+```bash
+# 使用Python
+python3 -m http.server 8000
+
+# 或使用Node.js
+npx http-server -p 8000
+```
+
+然后访问 `http://localhost:8000`
+
+## 使用步骤
+
+### 1. 连接钱包
+- 点击"连接钱包"按钮
+- 确保已安装MetaMask
+- 自动切换到BSC主网
+
+### 2. 选择转账类型
+
+#### BNB转账
+1. 选择"BNB (原生代币)"
+2. Gas Limit默认:21000
+
+#### BEP-20 Token转账
+1. 选择"BEP-20 Token"
+2. 输入Token合约地址
+3. 点击"加载Token信息"
+4. 确认Token信息正确
+5. Gas Limit默认:65000
+
+### 3. 准备转账数据
+
+格式:`地址,金额,备注`(每行一条)
+
+示例:
+```
+0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb,0.001,用户A
+0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0.002,用户B
+0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0.003,用户C
+```
+
+**注意:**
+- BNB转账:金额单位为BNB(如0.1表示0.1个BNB)
+- Token转账:金额为实际数量(如100表示100个token)
+- 备注为可选项,可以留空
+
+### 4. 输入转账数据
+- 在文本框中输入或粘贴转账数据
+- 每行一条记录,格式:地址,金额,备注
+- 点击"解析数据"按钮
+- 或使用快捷键 Ctrl/Cmd + Enter 快速解析
+
+### 5. 配置参数
+- **发送间隔**:每笔交易之间的延迟(毫秒)
+- **Gas Limit**:每笔交易的Gas限制
+
+### 6. 开始转账
+1. 点击"开始批量转账"
+2. 确认转账信息
+3. 勾选确认框
+4. 点击"开始执行"
+
+### 7. 查看转账历史
+1. 点击右上角"转账历史"按钮
+2. 查看所有历史转账记录
+3. 可按状态、类型筛选
+4. 支持搜索地址或交易哈希
+5. 可导出历史记录为CSV
+6. 点击"查看详情"查看单笔转账详情
+7. 历史记录保存在浏览器本地存储中
+
+## 常见Token合约地址(BSC主网)
+
+- **USDT**: 0x55d398326f99059fF775485246999027B3197955
+- **USDC**: 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d
+- **BUSD**: 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56
+- **CAKE**: 0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82
+
+## 注意事项
+
+⚠️ **安全提示**
+- 转账前请仔细核对收款地址
+- 确保钱包中有足够的BNB支付Gas费
+- Token转账需要足够的Token余额
+- 建议先小额测试
+
+⚠️ **Gas费用**
+- BNB转账:约21000 Gas
+- Token转账:约65000 Gas
+- 实际费用取决于网络拥堵情况
+
+⚠️ **网络要求**
+- 必须连接BSC主网(Chain ID: 56)
+- RPC节点:https://bsc-dataseed.binance.org/
+
+## 技术栈
+
+- HTML/CSS/JavaScript
+- ethers.js v5.7.2
+- MetaMask Web3 Provider
+- BSC (Binance Smart Chain)
+
+## 文件说明
+
+- `index.html` - 主页面
+- `history.html` - 转账历史页面
+- `app.js` - 应用逻辑
+- `batchTransfer.js` - 转账核心功能
+- `history.js` - 历史记录管理
+- `utils.js` - 工具函数
+- `style.css` - 样式文件
+- `example.csv` - CSV示例文件
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..8974d73
--- /dev/null
+++ b/app.js
@@ -0,0 +1,696 @@
+class BatchTransferApp {
+ constructor() {
+ this.transferData = [];
+ this.transferResults = [];
+ this.isTransferring = false;
+ this.isTokenTransfer = false;
+ this.tokenInfo = null;
+
+ this.initElements();
+ this.initEventListeners();
+ this.initUI();
+ }
+
+ initElements() {
+ // 钱包相关
+ this.connectWalletBtn = document.getElementById('connectWallet');
+ this.disconnectWalletBtn = document.getElementById('disconnectWallet');
+ this.walletInfo = document.getElementById('connectedInfo');
+ this.accountAddress = document.getElementById('accountAddress');
+ this.walletBalance = document.getElementById('walletBalance');
+ this.networkInfo = document.getElementById('networkInfo');
+
+ // 转账类型
+ this.transferTypeRadios = document.querySelectorAll('input[name="transferType"]');
+ this.tokenConfig = document.getElementById('tokenConfig');
+ this.tokenAddress = document.getElementById('tokenAddress');
+ this.loadTokenInfoBtn = document.getElementById('loadTokenInfo');
+ this.tokenInfoElement = document.getElementById('tokenInfo');
+ this.tokenName = document.getElementById('tokenName');
+ this.tokenSymbol = document.getElementById('tokenSymbol');
+ this.tokenBalance = document.getElementById('tokenBalance');
+
+ // 上传相关
+ this.transferInput = document.getElementById('transferInput');
+ this.parseDataBtn = document.getElementById('parseData');
+ this.clearInputBtn = document.getElementById('clearInput');
+ this.dataPreview = document.getElementById('dataPreview');
+ this.dataTable = document.getElementById('dataTable');
+ this.dataCount = document.getElementById('dataCount');
+ this.clearDataBtn = document.getElementById('clearData');
+
+ // 汇总信息
+ this.totalCount = document.getElementById('totalCount');
+ this.totalAmount = document.getElementById('totalAmount');
+ this.amountUnit = document.getElementById('amountUnit');
+ this.estimatedGas = document.getElementById('estimatedGas');
+ this.totalCost = document.getElementById('totalCost');
+
+ // 高级设置
+ this.delayTime = document.getElementById('delayTime');
+ this.gasLimit = document.getElementById('gasLimit');
+ this.gasPrice = document.getElementById('gasPrice');
+
+ // 转账按钮
+ this.startTransferBtn = document.getElementById('startTransfer');
+ this.stopTransferBtn = document.getElementById('stopTransfer');
+
+ // 进度相关
+ this.progressSection = document.getElementById('progressSection');
+ this.progressFill = document.getElementById('progressFill');
+ this.progressInfo = document.getElementById('progressInfo');
+ this.progressPercent = document.getElementById('progressPercent');
+ this.successCount = document.getElementById('successCount');
+ this.failedCount = document.getElementById('failedCount');
+ this.pendingCount = document.getElementById('pendingCount');
+
+ // 日志相关
+ this.transferLog = document.getElementById('transferLog');
+ this.clearLogBtn = document.getElementById('clearLog');
+
+ // 模态框
+ this.confirmModal = document.getElementById('confirmModal');
+ this.confirmCount = document.getElementById('confirmCount');
+ this.confirmAmount = document.getElementById('confirmAmount');
+ this.confirmGas = document.getElementById('confirmGas');
+ this.confirmBalance = document.getElementById('confirmBalance');
+ this.confirmCheck = document.getElementById('confirmCheck');
+ this.cancelTransferBtn = document.getElementById('cancelTransfer');
+ this.executeTransferBtn = document.getElementById('executeTransfer');
+ this.modalCloseBtn = document.querySelector('.modal-close');
+ }
+
+ initEventListeners() {
+ // 钱包连接
+ this.connectWalletBtn.addEventListener('click', () => this.connectWallet());
+ this.disconnectWalletBtn.addEventListener('click', () => this.disconnectWallet());
+
+ // 转账类型切换
+ this.transferTypeRadios.forEach(radio => {
+ radio.addEventListener('change', (e) => {
+ if (e.target.checked) {
+ this.onTransferTypeChange(e.target.value);
+ }
+ });
+ });
+
+ // Token相关
+ this.loadTokenInfoBtn.addEventListener('click', () => this.loadTokenInfo());
+ this.tokenAddress.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ this.loadTokenInfo();
+ }
+ });
+
+ // 数据输入
+ this.parseDataBtn.addEventListener('click', () => this.parseTransferData());
+ this.clearInputBtn.addEventListener('click', () => this.clearInput());
+ this.transferInput.addEventListener('keydown', (e) => {
+ // Ctrl/Cmd + Enter 快捷键解析数据
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
+ this.parseTransferData();
+ }
+ });
+
+ // 清空数据
+ if (this.clearDataBtn) {
+ this.clearDataBtn.addEventListener('click', () => this.clearData());
+ }
+
+ // 转账控制
+ this.startTransferBtn.addEventListener('click', () => this.showConfirmModal());
+ this.stopTransferBtn.addEventListener('click', () => this.stopTransfer());
+
+ // 日志控制
+ this.clearLogBtn.addEventListener('click', () => this.clearLog());
+
+ // 配置变化
+ this.delayTime.addEventListener('change', () => this.updateSummary());
+ this.gasLimit.addEventListener('change', () => this.updateSummary());
+
+ // 模态框
+ this.confirmCheck.addEventListener('change', (e) => {
+ this.executeTransferBtn.disabled = !e.target.checked;
+ });
+ this.cancelTransferBtn.addEventListener('click', () => this.hideModal());
+ this.executeTransferBtn.addEventListener('click', () => this.executeTransfer());
+ this.modalCloseBtn.addEventListener('click', () => this.hideModal());
+
+ // 点击模态框外部关闭
+ this.confirmModal.addEventListener('click', (e) => {
+ if (e.target === this.confirmModal) {
+ this.hideModal();
+ }
+ });
+ }
+
+ initUI() {
+ this.updateUI();
+
+ // 尝试恢复钱包连接
+ this.restoreWalletConnection();
+
+ // 定期更新Gas价格
+ setInterval(() => {
+ if (window.BatchTransfer.provider) {
+ this.updateGasPrice();
+ }
+ }, 30000);
+ }
+
+ async restoreWalletConnection() {
+ try {
+ // 检查是否有保存的钱包连接状态
+ const wasConnected = localStorage.getItem('walletConnected');
+ if (wasConnected === 'true' && window.ethereum) {
+ // 尝试自动连接
+ const accounts = await window.ethereum.request({
+ method: 'eth_accounts'
+ });
+
+ if (accounts && accounts.length > 0) {
+ // 钱包已授权,自动连接
+ await this.connectWallet();
+ }
+ }
+ } catch (error) {
+ console.log('无法恢复钱包连接:', error);
+ localStorage.removeItem('walletConnected');
+ }
+ }
+
+ async connectWallet() {
+ try {
+ this.addLog('正在连接钱包...', 'info');
+
+ const account = await window.BatchTransfer.connectWallet();
+
+ // 保存连接状态到localStorage
+ localStorage.setItem('walletConnected', 'true');
+
+ // 更新UI
+ this.connectWalletBtn.style.display = 'none';
+ this.walletInfo.style.display = 'flex';
+ this.accountAddress.textContent = window.TransferUtils.formatAddress(account);
+
+ // 获取余额和网络信息
+ await this.updateWalletInfo();
+
+ this.addLog('钱包连接成功', 'success');
+
+ } catch (error) {
+ this.addLog(`连接失败: ${error.message}`, 'error');
+ alert(`连接钱包失败: ${error.message}`);
+ }
+ }
+
+ async disconnectWallet() {
+ await window.BatchTransfer.disconnectWallet();
+
+ // 清除保存的连接状态
+ localStorage.removeItem('walletConnected');
+
+ // 重置UI
+ this.connectWalletBtn.style.display = 'block';
+ this.walletInfo.style.display = 'none';
+
+ // 检查networkInfo元素是否存在(因为我们已经从页面移除了它)
+ if (this.networkInfo) {
+ this.networkInfo.textContent = '未连接';
+ }
+ this.gasPrice.value = '0';
+
+ this.addLog('已断开钱包连接', 'info');
+ }
+
+ async updateWalletInfo() {
+ try {
+ // 更新余额
+ const balance = await window.BatchTransfer.getWalletBalance();
+ this.walletBalance.textContent = window.TransferUtils.formatNumber(balance, 4);
+
+ // 更新网络信息(如果元素存在)
+ if (this.networkInfo) {
+ const network = await window.BatchTransfer.getNetworkInfo();
+ this.networkInfo.textContent = `${network.name} (Chain ID: ${network.chainId})`;
+ }
+
+ // 更新Gas价格
+ await this.updateGasPrice();
+
+ } catch (error) {
+ console.error('更新钱包信息失败:', error);
+ }
+ }
+
+ async updateGasPrice() {
+ try {
+ const gasPrice = await window.BatchTransfer.getGasPrice();
+ this.gasPrice.value = window.TransferUtils.formatNumber(parseFloat(gasPrice), 2);
+ this.updateSummary();
+ } catch (error) {
+ console.error('获取Gas价格失败:', error);
+ }
+ }
+
+ onTransferTypeChange(type) {
+ this.isTokenTransfer = (type === 'token');
+
+ if (this.isTokenTransfer) {
+ // 显示Token配置
+ this.tokenConfig.style.display = 'block';
+ // 更新Gas Limit默认值
+ this.gasLimit.value = '65000';
+ this.amountUnit.textContent = 'Token';
+ } else {
+ // 隐藏Token配置
+ this.tokenConfig.style.display = 'none';
+ this.tokenInfoElement.style.display = 'none';
+ // 恢复BNB转账的Gas Limit
+ this.gasLimit.value = '21000';
+ this.amountUnit.textContent = 'BNB';
+ // 清空token信息
+ this.tokenInfo = null;
+ }
+
+ this.updateSummary();
+ }
+
+ async loadTokenInfo() {
+ const address = this.tokenAddress.value.trim();
+
+ if (!address) {
+ alert('请输入Token合约地址');
+ return;
+ }
+
+ if (!window.BatchTransfer.provider) {
+ alert('请先连接钱包');
+ return;
+ }
+
+ try {
+ this.addLog('正在加载Token信息...', 'info');
+ this.loadTokenInfoBtn.disabled = true;
+ this.loadTokenInfoBtn.innerHTML = ' 加载中...';
+
+ // 加载Token合约
+ const tokenInfo = await window.BatchTransfer.loadTokenContract(address);
+
+ // 获取Token余额
+ const balance = await window.BatchTransfer.getTokenBalance();
+
+ // 更新UI
+ this.tokenName.textContent = tokenInfo.name;
+ this.tokenSymbol.textContent = tokenInfo.symbol;
+ this.tokenBalance.textContent = window.TransferUtils.formatNumber(parseFloat(balance), 4);
+
+ this.tokenInfoElement.style.display = 'block';
+ this.tokenInfo = tokenInfo;
+
+ // 更新金额单位
+ this.amountUnit.textContent = tokenInfo.symbol;
+
+ this.addLog(`Token加载成功: ${tokenInfo.name} (${tokenInfo.symbol})`, 'success');
+
+ // 更新汇总信息
+ this.updateSummary();
+
+ } catch (error) {
+ this.addLog(`加载Token失败: ${error.message}`, 'error');
+ alert(`加载Token失败: ${error.message}`);
+ this.tokenInfoElement.style.display = 'none';
+ } finally {
+ this.loadTokenInfoBtn.disabled = false;
+ this.loadTokenInfoBtn.innerHTML = ' 加载';
+ }
+ }
+
+ loadCSVFile(file) {
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ try {
+ const csvText = e.target.result;
+ this.transferData = window.TransferUtils.parseCSV(csvText);
+
+ // 验证数据
+ const errors = window.BatchTransfer.validateTransferData(this.transferData);
+
+ if (errors.length > 0) {
+ this.addLog('CSV文件解析错误:', 'error');
+ errors.forEach(error => this.addLog(error, 'error'));
+ alert(`发现${errors.length}个错误,请检查CSV文件格式`);
+ return;
+ }
+
+ // 显示数据预览
+ this.dataPreview.style.display = 'block';
+
+ // 更新表格显示
+ this.updateDataTable();
+
+ // 更新汇总信息
+ this.updateSummary();
+
+ this.addLog(`成功加载 ${this.transferData.length} 条转账记录`, 'success');
+
+ } catch (error) {
+ this.addLog(`解析CSV文件失败: ${error.message}`, 'error');
+ alert('解析CSV文件失败,请检查文件格式');
+ }
+ };
+
+ reader.onerror = () => {
+ this.addLog('读取文件失败', 'error');
+ };
+
+ reader.readAsText(file);
+ }
+
+ parseTransferData() {
+ const inputText = this.transferInput.value.trim();
+
+ if (!inputText) {
+ alert('请输入转账数据');
+ return;
+ }
+
+ try {
+ // 使用相同的CSV解析函数
+ this.transferData = window.TransferUtils.parseCSV(inputText);
+
+ // 验证数据
+ const errors = window.BatchTransfer.validateTransferData(this.transferData);
+
+ if (errors.length > 0) {
+ this.addLog('数据解析错误:', 'error');
+ errors.forEach(error => this.addLog(error, 'error'));
+ alert(`发现${errors.length}个错误,请检查数据格式\n\n格式:地址,金额,备注\n示例:0x123...,0.1,用户A`);
+ return;
+ }
+
+ // 显示数据预览
+ this.dataPreview.style.display = 'block';
+
+ // 更新表格显示
+ this.updateDataTable();
+
+ // 更新汇总信息
+ this.updateSummary();
+
+ this.addLog(`成功解析 ${this.transferData.length} 条转账记录`, 'success');
+
+ } catch (error) {
+ this.addLog(`解析数据失败: ${error.message}`, 'error');
+ alert('解析数据失败,请检查数据格式\n\n格式:地址,金额,备注\n示例:0x123...,0.1,用户A');
+ }
+ }
+
+ clearInput() {
+ this.transferInput.value = '';
+ this.clearData();
+ }
+
+ updateDataTable() {
+ const tbody = this.dataTable.querySelector('tbody');
+ tbody.innerHTML = '';
+
+ this.transferData.forEach((item, index) => {
+ const row = document.createElement('tr');
+
+ row.innerHTML = `
+
${index + 1} |
+ ${window.TransferUtils.formatAddress(item.address)} |
+ ${item.amount} |
+ ${item.note || '-'} |
+ `;
+
+ tbody.appendChild(row);
+ });
+
+ this.dataCount.textContent = this.transferData.length;
+ }
+
+ clearData() {
+ this.transferData = [];
+ this.dataPreview.style.display = 'none';
+ this.updateSummary();
+ this.addLog('已清空转账数据', 'info');
+ }
+
+ async updateSummary() {
+ if (this.transferData.length === 0) {
+ this.totalCount.textContent = '0';
+ this.totalAmount.textContent = '0';
+ this.estimatedGas.textContent = '0';
+ this.totalCost.textContent = '0';
+ this.startTransferBtn.disabled = true;
+ return;
+ }
+
+ // 检查Token转账模式下是否已加载Token信息
+ if (this.isTokenTransfer && !this.tokenInfo) {
+ this.startTransferBtn.disabled = true;
+ return;
+ }
+
+ // 计算总金额
+ const totalAmount = this.transferData.reduce((sum, item) => sum + item.amount, 0);
+ this.totalCount.textContent = this.transferData.length;
+ this.totalAmount.textContent = window.TransferUtils.formatNumber(totalAmount, 4);
+
+ // 计算Gas费用
+ if (window.BatchTransfer.provider && this.gasPrice.value) {
+ const gasPrice = parseFloat(this.gasPrice.value);
+ const gasLimit = parseInt(this.gasLimit.value);
+
+ try {
+ const cost = await window.BatchTransfer.calculateTotalCost(
+ this.transferData,
+ gasLimit,
+ gasPrice,
+ this.isTokenTransfer
+ );
+
+ this.estimatedGas.textContent = window.TransferUtils.formatNumber(cost.totalGasCost, 4);
+ this.totalCost.textContent = window.TransferUtils.formatNumber(cost.totalCost, 4);
+ } catch (error) {
+ console.error('计算Gas费用失败:', error);
+ }
+ }
+
+ // 启用开始按钮
+ this.startTransferBtn.disabled = false;
+ }
+
+ showConfirmModal() {
+ if (!window.BatchTransfer.provider) {
+ alert('请先连接钱包');
+ return;
+ }
+
+ if (this.transferData.length === 0) {
+ alert('请先上传转账数据');
+ return;
+ }
+
+ // 更新确认信息
+ this.confirmCount.textContent = this.transferData.length;
+ this.confirmAmount.textContent = this.totalAmount.textContent + ' ' + this.amountUnit.textContent;
+ this.confirmGas.textContent = this.estimatedGas.textContent;
+ this.confirmBalance.textContent = this.walletBalance.textContent;
+
+ // 重置确认勾选
+ this.confirmCheck.checked = false;
+ this.executeTransferBtn.disabled = true;
+
+ // 显示模态框
+ this.confirmModal.style.display = 'flex';
+ }
+
+ hideModal() {
+ this.confirmModal.style.display = 'none';
+ }
+
+ async executeTransfer() {
+ this.hideModal();
+
+ if (!window.BatchTransfer.provider) {
+ this.addLog('请先连接钱包', 'error');
+ return;
+ }
+
+ // Token转账时检查Token余额
+ if (this.isTokenTransfer) {
+ if (!this.tokenInfo) {
+ this.addLog('请先加载Token信息', 'error');
+ return;
+ }
+
+ const tokenBalance = parseFloat(this.tokenBalance.textContent);
+ const requiredAmount = parseFloat(this.totalAmount.textContent);
+
+ if (tokenBalance < requiredAmount) {
+ this.addLog(`Token余额不足:需要 ${requiredAmount} ${this.tokenInfo.symbol},当前余额 ${tokenBalance} ${this.tokenInfo.symbol}`, 'error');
+ alert(`Token余额不足,请确保钱包中有足够的${this.tokenInfo.symbol}`);
+ return;
+ }
+
+ // 检查BNB余额是否足够支付Gas费
+ const bnbBalance = parseFloat(this.walletBalance.textContent);
+ const requiredGas = parseFloat(this.estimatedGas.textContent);
+
+ if (bnbBalance < requiredGas) {
+ this.addLog(`BNB余额不足以支付Gas费:需要 ${requiredGas} BNB,当前余额 ${bnbBalance} BNB`, 'error');
+ alert('BNB余额不足以支付Gas费用');
+ return;
+ }
+ } else {
+ // BNB转账时检查余额
+ const balance = parseFloat(this.walletBalance.textContent);
+ const required = parseFloat(this.totalCost.textContent);
+
+ if (balance < required) {
+ this.addLog(`余额不足:需要 ${required} BNB,当前余额 ${balance} BNB`, 'error');
+ alert('余额不足,请确保钱包中有足够的BNB支付转账费用');
+ return;
+ }
+ }
+
+ // 显示进度区域
+ this.progressSection.style.display = 'block';
+ this.progressSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+
+ // 开始转账
+ this.isTransferring = true;
+ this.startTransferBtn.style.display = 'none';
+ this.stopTransferBtn.style.display = 'block';
+
+ // 重置进度
+ this.resetProgress();
+
+ const transferType = this.isTokenTransfer ? `Token (${this.tokenInfo.symbol})` : 'BNB';
+ this.addLog(`开始批量转账 ${transferType}...`, 'info');
+
+ try {
+ const options = {
+ delay: parseInt(this.delayTime.value),
+ gasLimit: parseInt(this.gasLimit.value),
+ isTokenTransfer: this.isTokenTransfer,
+ onProgress: (progress) => this.updateProgress(progress),
+ onComplete: (results) => this.onTransferComplete(results),
+ onError: (error) => this.onTransferError(error)
+ };
+
+ this.transferResults = await window.BatchTransfer.executeBatchTransfer(
+ this.transferData,
+ options
+ );
+
+ } catch (error) {
+ this.addLog(`转账过程出错: ${error.message}`, 'error');
+ this.stopTransfer();
+ }
+ }
+
+ stopTransfer() {
+ window.BatchTransfer.stopTransfer();
+ this.isTransferring = false;
+ this.startTransferBtn.style.display = 'block';
+ this.stopTransferBtn.style.display = 'none';
+ this.addLog('转账已停止', 'warning');
+ }
+
+ resetProgress() {
+ this.progressFill.style.width = '0%';
+ this.progressInfo.textContent = '0/0';
+ this.progressPercent.textContent = '0%';
+ this.successCount.textContent = '0';
+ this.failedCount.textContent = '0';
+ this.pendingCount.textContent = this.transferData.length.toString();
+ }
+
+ updateProgress(progress) {
+ const percent = Math.round((progress.current / progress.total) * 100);
+
+ this.progressFill.style.width = `${percent}%`;
+ this.progressInfo.textContent = `${progress.current}/${progress.total}`;
+ this.progressPercent.textContent = `${percent}%`;
+ this.successCount.textContent = progress.success.toString();
+ this.failedCount.textContent = progress.failed.toString();
+ this.pendingCount.textContent = (progress.total - progress.current).toString();
+
+ // 更新余额(每5笔更新一次)
+ if (progress.current % 5 === 0) {
+ this.updateWalletInfo();
+ }
+ }
+
+ onTransferComplete(results) {
+ this.isTransferring = false;
+ this.startTransferBtn.style.display = 'block';
+ this.stopTransferBtn.style.display = 'none';
+
+ const successCount = results.filter(r => r.success).length;
+ const failedCount = results.filter(r => !r.success).length;
+
+ this.addLog(`转账完成!成功: ${successCount},失败: ${failedCount}`, 'success');
+
+ // 更新钱包余额
+ this.updateWalletInfo();
+
+ // 询问是否导出结果
+ if (confirm('转账完成,是否导出详细结果?')) {
+ window.TransferUtils.exportToCSV(results);
+ }
+ }
+
+ onTransferError(error) {
+ this.addLog(`转账过程中出错: ${error.message}`, 'error');
+ this.isTransferring = false;
+ this.startTransferBtn.style.display = 'block';
+ this.stopTransferBtn.style.display = 'none';
+ }
+
+ addLog(message, type = 'info') {
+ const log = window.TransferUtils.generateLogMessage(type, message);
+
+ // 移除空状态
+ const emptyState = this.transferLog.querySelector('.log-empty');
+ if (emptyState) {
+ emptyState.remove();
+ }
+
+ // 添加到日志顶部
+ this.transferLog.insertAdjacentHTML('afterbegin', log.html);
+
+ // 限制日志数量
+ const logItems = this.transferLog.querySelectorAll('.log-item');
+ if (logItems.length > 100) {
+ logItems[logItems.length - 1].remove();
+ }
+ }
+
+ clearLog() {
+ this.transferLog.innerHTML = `
+
+ `;
+ }
+
+ updateUI() {
+ // 初始化UI状态
+ this.startTransferBtn.disabled = true;
+ this.stopTransferBtn.style.display = 'none';
+ this.progressSection.style.display = 'none';
+ this.dataPreview.style.display = 'none';
+ }
+}
+
+// 应用初始化
+document.addEventListener('DOMContentLoaded', () => {
+ window.app = new BatchTransferApp();
+});
diff --git a/batchTransfer.js b/batchTransfer.js
new file mode 100644
index 0000000..5392d21
--- /dev/null
+++ b/batchTransfer.js
@@ -0,0 +1,435 @@
+class BSCBatchTransfer {
+ constructor() {
+ this.provider = null;
+ this.signer = null;
+ this.account = '';
+ this.isTransferring = false;
+ this.currentBatch = 0;
+ this.totalBatches = 0;
+
+ // BEP-20 Token ABI (与ERC-20兼容)
+ this.BEP20_ABI = [
+ "function name() view returns (string)",
+ "function symbol() view returns (string)",
+ "function decimals() view returns (uint8)",
+ "function totalSupply() view returns (uint256)",
+ "function balanceOf(address) view returns (uint256)",
+ "function transfer(address to, uint256 amount) returns (bool)",
+ "function allowance(address owner, address spender) view returns (uint256)",
+ "function approve(address spender, uint256 amount) returns (bool)"
+ ];
+
+ this.tokenContract = null;
+ this.tokenInfo = null;
+ }
+
+ async waitForEthereum(maxWait = 3000) {
+ return new Promise((resolve) => {
+ if (window.ethereum) {
+ resolve(true);
+ return;
+ }
+
+ let waited = 0;
+ const interval = setInterval(() => {
+ if (window.ethereum) {
+ clearInterval(interval);
+ resolve(true);
+ } else if (waited >= maxWait) {
+ clearInterval(interval);
+ resolve(false);
+ } else {
+ waited += 100;
+ }
+ }, 100);
+ });
+ }
+
+ async connectWallet() {
+ try {
+ console.log('开始连接钱包...');
+
+ // 等待MetaMask注入,最多等待3秒
+ const ethereumAvailable = await this.waitForEthereum();
+
+ if (!ethereumAvailable || !window.ethereum) {
+ console.error('未检测到MetaMask');
+ throw new Error('请安装MetaMask或支持BSC的钱包插件,刷新页面后重试');
+ }
+
+ console.log('检测到钱包:', window.ethereum.isMetaMask ? 'MetaMask' : '其他钱包');
+
+ // 请求连接钱包
+ const accounts = await window.ethereum.request({
+ method: 'eth_requestAccounts'
+ });
+
+ this.account = accounts[0];
+
+ // 切换到BSC网络
+ await this.switchToBSCNetwork();
+
+ // 创建Provider和Signer
+ this.provider = new ethers.providers.Web3Provider(window.ethereum);
+ this.signer = this.provider.getSigner();
+
+ // 监听账户变化
+ window.ethereum.on('accountsChanged', (accounts) => {
+ if (accounts.length === 0) {
+ this.disconnectWallet();
+ } else {
+ this.account = accounts[0];
+ this.updateWalletInfo();
+ }
+ });
+
+ // 监听网络变化
+ window.ethereum.on('chainChanged', (chainId) => {
+ window.location.reload();
+ });
+
+ return this.account;
+
+ } catch (error) {
+ console.error('连接钱包失败:', error);
+ throw error;
+ }
+ }
+
+ async disconnectWallet() {
+ this.provider = null;
+ this.signer = null;
+ this.account = '';
+ this.isTransferring = false;
+
+ // 移除事件监听
+ if (window.ethereum) {
+ window.ethereum.removeAllListeners('accountsChanged');
+ window.ethereum.removeAllListeners('chainChanged');
+ }
+ }
+
+ async switchToBSCNetwork() {
+ const chainId = await window.ethereum.request({ method: 'eth_chainId' });
+
+ // BSC主网chainId: 0x38 (56)
+ if (chainId !== '0x38') {
+ try {
+ await window.ethereum.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId: '0x38' }]
+ });
+ } catch (switchError) {
+ // 如果网络不存在,添加网络
+ if (switchError.code === 4902) {
+ await window.ethereum.request({
+ method: 'wallet_addEthereumChain',
+ params: [{
+ chainId: '0x38',
+ chainName: 'Binance Smart Chain',
+ nativeCurrency: {
+ name: 'BNB',
+ symbol: 'BNB',
+ decimals: 18
+ },
+ rpcUrls: ['https://bsc-dataseed.binance.org/'],
+ blockExplorerUrls: ['https://bscscan.com/']
+ }]
+ });
+ } else {
+ throw switchError;
+ }
+ }
+ }
+ }
+
+ async getWalletBalance() {
+ if (!this.provider) throw new Error('请先连接钱包');
+
+ const balance = await this.provider.getBalance(this.account);
+ return ethers.utils.formatEther(balance);
+ }
+
+ async loadTokenContract(tokenAddress) {
+ if (!this.provider) throw new Error('请先连接钱包');
+
+ if (!ethers.utils.isAddress(tokenAddress)) {
+ throw new Error('无效的Token合约地址');
+ }
+
+ try {
+ this.tokenContract = new ethers.Contract(tokenAddress, this.BEP20_ABI, this.signer);
+
+ // 获取Token信息
+ const [name, symbol, decimals] = await Promise.all([
+ this.tokenContract.name(),
+ this.tokenContract.symbol(),
+ this.tokenContract.decimals()
+ ]);
+
+ this.tokenInfo = {
+ address: tokenAddress,
+ name,
+ symbol,
+ decimals
+ };
+
+ return this.tokenInfo;
+ } catch (error) {
+ this.tokenContract = null;
+ this.tokenInfo = null;
+ throw new Error('无法加载Token信息,请检查合约地址是否正确');
+ }
+ }
+
+ async getTokenBalance() {
+ if (!this.tokenContract || !this.tokenInfo) {
+ throw new Error('请先加载Token信息');
+ }
+
+ const balance = await this.tokenContract.balanceOf(this.account);
+ return ethers.utils.formatUnits(balance, this.tokenInfo.decimals);
+ }
+
+ async getGasPrice() {
+ if (!this.provider) throw new Error('请先连接钱包');
+
+ const gasPrice = await this.provider.getGasPrice();
+ return ethers.utils.formatUnits(gasPrice, 'gwei');
+ }
+
+ async getNetworkInfo() {
+ if (!this.provider) throw new Error('请先连接钱包');
+
+ const network = await this.provider.getNetwork();
+ return {
+ name: network.name,
+ chainId: network.chainId
+ };
+ }
+
+ async validateTransferData(transferData) {
+ const errors = [];
+
+ for (let i = 0; i < transferData.length; i++) {
+ const item = transferData[i];
+
+ // 验证地址
+ if (!ethers.utils.isAddress(item.address)) {
+ errors.push(`第${i + 1}行:无效的地址 ${item.address}`);
+ }
+
+ // 验证金额
+ try {
+ const amount = parseFloat(item.amount);
+ if (isNaN(amount) || amount <= 0) {
+ errors.push(`第${i + 1}行:无效的金额 ${item.amount}`);
+ }
+ } catch (error) {
+ errors.push(`第${i + 1}行:无效的金额格式`);
+ }
+ }
+
+ return errors;
+ }
+
+ async calculateTotalCost(transferData, gasLimit, gasPrice, isTokenTransfer = false) {
+ if (!this.provider) throw new Error('请先连接钱包');
+
+ const totalAmount = transferData.reduce((sum, item) => {
+ return sum + parseFloat(item.amount);
+ }, 0);
+
+ // 计算Gas费用
+ const gasCostPerTx = ethers.utils.parseUnits(gasPrice.toString(), 'gwei')
+ .mul(gasLimit)
+ .mul(transferData.length);
+
+ const totalGasCost = parseFloat(ethers.utils.formatEther(gasCostPerTx));
+
+ return {
+ totalAmount,
+ totalGasCost,
+ // Token转账时,总成本只包含Gas费用(因为token金额不是BNB)
+ totalCost: isTokenTransfer ? totalGasCost : (totalAmount + totalGasCost)
+ };
+ }
+
+ async executeBatchTransfer(transferData, options) {
+ if (!this.signer) throw new Error('请先连接钱包');
+
+ const {
+ delay = 1000,
+ gasLimit = 21000,
+ isTokenTransfer = false,
+ onProgress,
+ onComplete,
+ onError
+ } = options;
+
+ // 如果是Token转账,检查是否已加载Token合约
+ if (isTokenTransfer && !this.tokenContract) {
+ throw new Error('请先加载Token合约信息');
+ }
+
+ this.isTransferring = true;
+ const results = [];
+
+ try {
+ for (let i = 0; i < transferData.length; i++) {
+ if (!this.isTransferring) {
+ throw new Error('转账被用户停止');
+ }
+
+ const transfer = transferData[i];
+
+ try {
+ // 获取当前Gas价格
+ const currentGasPrice = await this.provider.getGasPrice();
+
+ let txResponse;
+
+ if (isTokenTransfer) {
+ // Token转账
+ const amount = ethers.utils.parseUnits(
+ transfer.amount.toString(),
+ this.tokenInfo.decimals
+ );
+
+ // 发送Token转账交易
+ txResponse = await this.tokenContract.transfer(
+ transfer.address,
+ amount,
+ {
+ gasLimit: gasLimit,
+ gasPrice: currentGasPrice
+ }
+ );
+ } else {
+ // BNB转账
+ const tx = {
+ to: transfer.address,
+ value: ethers.utils.parseEther(transfer.amount.toString()),
+ gasLimit: gasLimit,
+ gasPrice: currentGasPrice
+ };
+
+ txResponse = await this.signer.sendTransaction(tx);
+ }
+
+ // 等待交易确认
+ const receipt = await txResponse.wait();
+
+ results.push({
+ index: i,
+ success: true,
+ hash: txResponse.hash,
+ to: transfer.address,
+ amount: transfer.amount,
+ receipt: receipt
+ });
+
+ // 保存到历史记录
+ this.saveTransferToHistory(transfer, results[results.length - 1], isTokenTransfer);
+
+ // 更新进度
+ if (onProgress) {
+ onProgress({
+ current: i + 1,
+ total: transferData.length,
+ success: results.filter(r => r.success).length,
+ failed: results.filter(r => !r.success).length
+ });
+ }
+
+ } catch (error) {
+ results.push({
+ index: i,
+ success: false,
+ to: transfer.address,
+ amount: transfer.amount,
+ error: error.message
+ });
+
+ // 保存到历史记录(包括失败的)
+ this.saveTransferToHistory(transfer, results[results.length - 1], isTokenTransfer);
+
+ // 更新进度
+ if (onProgress) {
+ onProgress({
+ current: i + 1,
+ total: transferData.length,
+ success: results.filter(r => r.success).length,
+ failed: results.filter(r => !r.success).length
+ });
+ }
+ }
+
+ // 延迟处理下一笔交易
+ if (i < transferData.length - 1) {
+ await this.delay(delay);
+ }
+ }
+
+ // 完成回调
+ if (onComplete) {
+ onComplete(results);
+ }
+
+ return results;
+
+ } catch (error) {
+ if (onError) {
+ onError(error);
+ }
+ throw error;
+ } finally {
+ this.isTransferring = false;
+ }
+ }
+
+ stopTransfer() {
+ this.isTransferring = false;
+ }
+
+ delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ // 保存转账记录到历史
+ saveTransferToHistory(transferData, result, isTokenTransfer = false) {
+ try {
+ const storageKey = 'bsc_transfer_history';
+ const history = JSON.parse(localStorage.getItem(storageKey) || '[]');
+
+ const record = {
+ id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ timestamp: Date.now(),
+ type: isTokenTransfer ? 'token' : 'bnb',
+ address: result.to,
+ amount: result.amount,
+ status: result.success ? 'success' : 'failed',
+ txHash: result.hash || null,
+ note: transferData.note || '',
+ gasCost: result.receipt ? ethers.utils.formatEther(
+ result.receipt.gasUsed.mul(result.receipt.effectiveGasPrice)
+ ) : null,
+ tokenAddress: isTokenTransfer && this.tokenInfo ? this.tokenInfo.address : null,
+ tokenSymbol: isTokenTransfer && this.tokenInfo ? this.tokenInfo.symbol : null,
+ error: result.error || null
+ };
+
+ history.push(record);
+ localStorage.setItem(storageKey, JSON.stringify(history));
+ } catch (error) {
+ console.error('保存转账历史失败:', error);
+ }
+ }
+
+ updateWalletInfo() {
+ // 这个方法需要在UI层实现,这里只提供接口
+ }
+}
+
+// 导出单例实例
+window.BatchTransfer = new BSCBatchTransfer();
\ No newline at end of file
diff --git a/developer.html b/developer.html
new file mode 100644
index 0000000..44354fc
--- /dev/null
+++ b/developer.html
@@ -0,0 +1,367 @@
+
+
+
+
+
+ 开发者介绍 | 区块链龙哥 出品
+
+
+
+
+
+
+
+
+
+ 返回主页
+
+
+
+
+
+
+
+
区块链龙哥
+
区块链开发者 · Web3工具开发
+
+ 专注于区块链技术开发和Web3工具创建,致力于为用户提供简单、安全、高效的区块链应用解决方案。
+ 擅长智能合约开发、DApp开发以及区块链工具开发。
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example.csv b/example.csv
new file mode 100644
index 0000000..75747b7
--- /dev/null
+++ b/example.csv
@@ -0,0 +1,3 @@
+0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb,0.001,测试地址1
+0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,0.002,测试地址2
+0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,0.003,测试地址3
diff --git a/history.html b/history.html
new file mode 100644
index 0000000..d3d267d
--- /dev/null
+++ b/history.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+ 转账历史 | 区块链龙哥 出品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 时间 |
+ 类型 |
+ 收款地址 |
+ 金额 |
+ 状态 |
+ 交易哈希 |
+ 备注 |
+ 操作 |
+
+
+
+
+ |
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 时间
+ -
+
+
+ 类型
+ -
+
+
+ 收款地址
+ -
+
+
+ 金额
+ -
+
+
+ 状态
+ -
+
+
+ 交易哈希
+ -
+
+
+ 备注
+ -
+
+
+ Gas费用
+ -
+
+
+
+
+
+
+
+
+
+
+
diff --git a/history.js b/history.js
new file mode 100644
index 0000000..5645e43
--- /dev/null
+++ b/history.js
@@ -0,0 +1,322 @@
+// 转账历史管理
+class TransferHistory {
+ constructor() {
+ this.storageKey = 'bsc_transfer_history';
+ this.history = this.loadHistory();
+ this.filteredHistory = [...this.history];
+ this.initElements();
+ this.bindEvents();
+ this.render();
+ }
+
+ initElements() {
+ // 筛选器
+ this.statusFilter = document.getElementById('statusFilter');
+ this.typeFilter = document.getElementById('typeFilter');
+ this.searchInput = document.getElementById('searchInput');
+
+ // 操作按钮
+ this.exportBtn = document.getElementById('exportHistory');
+ this.clearBtn = document.getElementById('clearHistory');
+
+ // 统计
+ this.totalSuccess = document.getElementById('totalSuccess');
+ this.totalFailed = document.getElementById('totalFailed');
+ this.totalPending = document.getElementById('totalPending');
+ this.totalTransfers = document.getElementById('totalTransfers');
+ this.recordCount = document.getElementById('recordCount');
+
+ // 表格
+ this.tableBody = document.getElementById('historyTableBody');
+
+ // 模态框
+ this.detailModal = document.getElementById('detailModal');
+ this.closeDetailBtn = document.getElementById('closeDetail');
+ this.viewOnBscscanBtn = document.getElementById('viewOnBscscan');
+ }
+
+ bindEvents() {
+ // 筛选器事件
+ this.statusFilter.addEventListener('change', () => this.applyFilters());
+ this.typeFilter.addEventListener('change', () => this.applyFilters());
+ this.searchInput.addEventListener('input', () => this.applyFilters());
+
+ // 操作按钮
+ this.exportBtn.addEventListener('click', () => this.exportToCSV());
+ this.clearBtn.addEventListener('click', () => this.clearHistory());
+
+ // 模态框
+ this.closeDetailBtn.addEventListener('click', () => this.closeModal());
+ this.detailModal.querySelector('.modal-close').addEventListener('click', () => this.closeModal());
+ this.detailModal.addEventListener('click', (e) => {
+ if (e.target === this.detailModal) this.closeModal();
+ });
+ }
+
+ // 从localStorage加载历史记录
+ loadHistory() {
+ try {
+ const data = localStorage.getItem(this.storageKey);
+ return data ? JSON.parse(data) : [];
+ } catch (error) {
+ console.error('加载历史记录失败:', error);
+ return [];
+ }
+ }
+
+ // 保存历史记录到localStorage
+ saveHistory() {
+ try {
+ localStorage.setItem(this.storageKey, JSON.stringify(this.history));
+ } catch (error) {
+ console.error('保存历史记录失败:', error);
+ showNotification('保存失败', 'error');
+ }
+ }
+
+ // 应用筛选
+ applyFilters() {
+ const status = this.statusFilter.value;
+ const type = this.typeFilter.value;
+ const search = this.searchInput.value.toLowerCase().trim();
+
+ this.filteredHistory = this.history.filter(record => {
+ // 状态筛选
+ if (status !== 'all' && record.status !== status) return false;
+
+ // 类型筛选
+ if (type !== 'all' && record.type !== type) return false;
+
+ // 搜索筛选
+ if (search) {
+ const searchableText = [
+ record.address,
+ record.txHash,
+ record.note
+ ].join(' ').toLowerCase();
+
+ if (!searchableText.includes(search)) return false;
+ }
+
+ return true;
+ });
+
+ this.render();
+ }
+
+ // 渲染页面
+ render() {
+ this.renderStats();
+ this.renderTable();
+ }
+
+ // 渲染统计数据
+ renderStats() {
+ const stats = {
+ success: 0,
+ failed: 0,
+ pending: 0,
+ total: this.history.length
+ };
+
+ this.history.forEach(record => {
+ if (record.status === 'success') stats.success++;
+ else if (record.status === 'failed') stats.failed++;
+ else if (record.status === 'pending') stats.pending++;
+ });
+
+ this.totalSuccess.textContent = stats.success;
+ this.totalFailed.textContent = stats.failed;
+ this.totalPending.textContent = stats.pending;
+ this.totalTransfers.textContent = stats.total;
+ this.recordCount.textContent = this.filteredHistory.length;
+ }
+
+ // 渲染表格
+ renderTable() {
+ if (this.filteredHistory.length === 0) {
+ this.tableBody.innerHTML = `
+
+
+
+
+ ${this.history.length === 0 ? '暂无转账记录' : '没有符合条件的记录'}
+
+ |
+
+ `;
+ return;
+ }
+
+ // 按时间倒序排列
+ const sortedHistory = [...this.filteredHistory].sort((a, b) => b.timestamp - a.timestamp);
+
+ this.tableBody.innerHTML = sortedHistory.map(record => `
+
+ | ${this.formatTime(record.timestamp)} |
+
+
+ ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || 'Token'}
+
+ |
+
+
+ ${this.formatAddress(record.address)}
+
+ |
+
+ ${record.amount} ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || ''}
+ |
+
+
+ ${this.getStatusText(record.status)}
+
+ |
+
+ ${record.txHash ? `
+
+ ${this.formatTxHash(record.txHash)}
+
+
+ ` : '-'}
+ |
+ ${record.note || '-'} |
+
+
+ |
+
+ `).join('');
+ }
+
+ // 显示详情模态框
+ showDetail(recordId) {
+ const record = this.history.find(r => r.id === recordId);
+ if (!record) return;
+
+ document.getElementById('detailTime').textContent = this.formatTime(record.timestamp, true);
+ document.getElementById('detailType').textContent = record.type === 'bnb' ? 'BNB' : `${record.tokenSymbol || 'Token'} (${record.tokenAddress})`;
+ document.getElementById('detailAddress').textContent = record.address;
+ document.getElementById('detailAmount').textContent = `${record.amount} ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || ''}`;
+ document.getElementById('detailStatus').innerHTML = `${this.getStatusText(record.status)}`;
+ document.getElementById('detailTxHash').textContent = record.txHash || '-';
+ document.getElementById('detailNote').textContent = record.note || '-';
+ document.getElementById('detailGas').textContent = record.gasCost ? `${record.gasCost} BNB` : '-';
+
+ if (record.txHash) {
+ this.viewOnBscscanBtn.href = `https://bscscan.com/tx/${record.txHash}`;
+ this.viewOnBscscanBtn.style.display = 'inline-flex';
+ } else {
+ this.viewOnBscscanBtn.style.display = 'none';
+ }
+
+ this.detailModal.style.display = 'flex';
+ }
+
+ // 关闭模态框
+ closeModal() {
+ this.detailModal.style.display = 'none';
+ }
+
+ // 导出为CSV
+ exportToCSV() {
+ if (this.filteredHistory.length === 0) {
+ showNotification('没有可导出的记录', 'warning');
+ return;
+ }
+
+ const headers = ['时间', '类型', '收款地址', '金额', '状态', '交易哈希', '备注', 'Gas费用'];
+ const rows = this.filteredHistory.map(record => [
+ this.formatTime(record.timestamp, true),
+ record.type === 'bnb' ? 'BNB' : record.tokenSymbol || 'Token',
+ record.address,
+ `${record.amount} ${record.type === 'bnb' ? 'BNB' : record.tokenSymbol || ''}`,
+ this.getStatusText(record.status),
+ record.txHash || '',
+ record.note || '',
+ record.gasCost || ''
+ ]);
+
+ const csvContent = [headers, ...rows]
+ .map(row => row.map(cell => `"${cell}"`).join(','))
+ .join('\n');
+
+ const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = `transfer_history_${Date.now()}.csv`;
+ link.click();
+
+ showNotification('导出成功', 'success');
+ }
+
+ // 清空历史记录
+ clearHistory() {
+ if (this.history.length === 0) {
+ showNotification('没有可清空的记录', 'warning');
+ return;
+ }
+
+ if (!confirm('确定要清空所有历史记录吗?此操作不可恢复!')) {
+ return;
+ }
+
+ this.history = [];
+ this.filteredHistory = [];
+ this.saveHistory();
+ this.render();
+ showNotification('历史记录已清空', 'success');
+ }
+
+ // 格式化时间
+ formatTime(timestamp, full = false) {
+ const date = new Date(timestamp);
+ if (full) {
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ });
+ }
+ return date.toLocaleString('zh-CN', {
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ }
+
+ // 格式化地址
+ formatAddress(address) {
+ if (!address) return '-';
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+ }
+
+ // 格式化交易哈希
+ formatTxHash(txHash) {
+ if (!txHash) return '-';
+ return `${txHash.slice(0, 8)}...${txHash.slice(-6)}`;
+ }
+
+ // 获取状态文本
+ getStatusText(status) {
+ const statusMap = {
+ 'success': '成功',
+ 'failed': '失败',
+ 'pending': '待确认'
+ };
+ return statusMap[status] || status;
+ }
+}
+
+// 初始化
+let historyManager;
+document.addEventListener('DOMContentLoaded', () => {
+ historyManager = new TransferHistory();
+ // 将实例暴露到全局,供onclick使用
+ window.history = historyManager;
+});
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d6692d5
--- /dev/null
+++ b/index.html
@@ -0,0 +1,308 @@
+
+
+
+
+
+ BSC批量转账工具 | 区块链龙哥 出品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 名称
+ -
+
+
+ 符号
+ -
+
+
+ 余额
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 序号 |
+ 收款地址 |
+ 金额 |
+ 备注 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 转账数量
+ 0 笔
+
+
+ 转账总额
+ 0 BNB
+
+
+ 预计Gas费
+ 0 BNB
+
+
+ 总计费用
+ 0 BNB
+
+
+
+
+
+
+ 高级设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Gwei
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
转账进度
+
+
+
+
+ 成功 0
+
+
+
+ 失败 0
+
+
+
+ 等待 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
请仔细核对以下信息:
+
+
转账数量0 笔
+
转账金额0
+
Gas费用0 BNB
+
账户余额0 BNB
+
+
+
+ 转账一旦开始将无法撤销,请确保信息正确!
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..92eb94e
--- /dev/null
+++ b/style.css
@@ -0,0 +1,1394 @@
+/* ==================== 全局样式 ==================== */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ /* BSC品牌色系 */
+ --primary-color: #F0B90B;
+ --primary-dark: #D9A00A;
+ --primary-light: #FFF8E1;
+
+ /* 辅助色 */
+ --success-color: #10B981;
+ --error-color: #EF4444;
+ --warning-color: #F59E0B;
+ --info-color: #3B82F6;
+
+ /* 中性色 */
+ --bg-color: #F9FAFB;
+ --card-bg: #FFFFFF;
+ --border-color: #E5E7EB;
+ --text-primary: #111827;
+ --text-secondary: #6B7280;
+ --text-tertiary: #9CA3AF;
+
+ /* 阴影 */
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+
+ /* 圆角 */
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+
+ /* 间距 */
+ --spacing-xs: 8px;
+ --spacing-sm: 12px;
+ --spacing-md: 16px;
+ --spacing-lg: 24px;
+ --spacing-xl: 32px;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
+ background: var(--bg-color);
+ color: var(--text-primary);
+ line-height: 1.6;
+ min-height: 100vh;
+ padding: var(--spacing-lg);
+}
+
+.container {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+/* ==================== 头部样式 ==================== */
+.header {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg) var(--spacing-xl);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-lg);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-color);
+}
+
+.header-content h1 {
+ color: var(--text-primary);
+ font-size: 24px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ margin-bottom: 4px;
+}
+
+.header-content h1 i {
+ color: var(--primary-color);
+}
+
+.subtitle {
+ color: var(--text-secondary);
+ font-size: 14px;
+ margin: 0;
+}
+
+.wallet-section {
+ display: flex;
+ align-items: center;
+}
+
+.wallet-connected {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ background: var(--bg-color);
+ padding: var(--spacing-sm) var(--spacing-md);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-color);
+}
+
+.wallet-details {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.wallet-address {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.wallet-balance {
+ font-size: 13px;
+ color: var(--success-color);
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.btn-icon {
+ background: none;
+ border: none;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ padding: var(--spacing-xs);
+ border-radius: var(--radius-sm);
+ transition: all 0.2s;
+}
+
+.btn-icon:hover {
+ background: var(--border-color);
+ color: var(--text-primary);
+}
+
+/* ==================== 按钮样式 ==================== */
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: var(--radius-md);
+ font-weight: 600;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-xs);
+ white-space: nowrap;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn:active {
+ transform: translateY(0);
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn-primary {
+ background: var(--primary-color);
+ color: var(--text-primary);
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--primary-dark);
+}
+
+.btn-secondary {
+ background: var(--bg-color);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+}
+
+.btn-secondary:hover {
+ background: #F3F4F6;
+ color: var(--text-primary);
+}
+
+.btn-danger {
+ background: var(--error-color);
+ color: white;
+}
+
+.btn-danger:hover {
+ background: #DC2626;
+}
+
+.btn-large {
+ padding: 14px 28px;
+ font-size: 16px;
+ width: 100%;
+}
+
+.btn-text {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 13px;
+ padding: 4px 8px;
+ border-radius: var(--radius-sm);
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.btn-text:hover {
+ background: var(--bg-color);
+ color: var(--text-primary);
+}
+
+/* ==================== 主内容区 ==================== */
+.main-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+ margin-bottom: var(--spacing-lg);
+}
+
+/* ==================== 步骤卡片 ==================== */
+.step-card {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-xl);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-color);
+}
+
+.step-header {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-lg);
+}
+
+.step-number {
+ width: 40px;
+ height: 40px;
+ background: var(--primary-color);
+ color: var(--text-primary);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ font-weight: 700;
+ flex-shrink: 0;
+}
+
+.step-header h2 {
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.step-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+}
+
+/* ==================== 转账类型选择器 ==================== */
+.transfer-type-selector {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--spacing-md);
+}
+
+.type-option {
+ cursor: pointer;
+}
+
+.type-option input[type="radio"] {
+ display: none;
+}
+
+.type-card {
+ background: var(--bg-color);
+ border: 2px solid var(--border-color);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ text-align: center;
+ transition: all 0.2s;
+}
+
+.type-card i {
+ font-size: 36px;
+ color: var(--text-tertiary);
+ margin-bottom: var(--spacing-sm);
+}
+
+.type-card h3 {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.type-card p {
+ font-size: 13px;
+ color: var(--text-secondary);
+ margin: 0;
+}
+
+.type-option input:checked + .type-card {
+ border-color: var(--primary-color);
+ background: var(--primary-light);
+}
+
+.type-option input:checked + .type-card i {
+ color: var(--primary-color);
+}
+
+.type-option:hover .type-card {
+ border-color: var(--primary-color);
+}
+
+/* ==================== Token配置 ==================== */
+.token-config {
+ padding-top: var(--spacing-md);
+ border-top: 1px solid var(--border-color);
+}
+
+.input-group {
+ margin-bottom: var(--spacing-md);
+}
+
+.input-group label {
+ display: block;
+ margin-bottom: var(--spacing-xs);
+ color: var(--text-secondary);
+ font-weight: 500;
+ font-size: 14px;
+}
+
+.input-with-button {
+ display: flex;
+ gap: var(--spacing-sm);
+}
+
+.input-with-button input {
+ flex: 1;
+}
+
+.form-control {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: 14px;
+ transition: all 0.2s ease;
+ background: var(--card-bg);
+ color: var(--text-primary);
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px var(--primary-light);
+}
+
+.token-info {
+ background: var(--bg-color);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-color);
+}
+
+.info-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--spacing-md);
+}
+
+.info-item {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.info-item .label {
+ font-size: 12px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.info-item .value {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+/* ==================== 上传区域 ==================== */
+.transfer-textarea {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: 14px;
+ font-family: 'SF Mono', Monaco, monospace;
+ line-height: 1.6;
+ resize: vertical;
+ min-height: 200px;
+ background: var(--card-bg);
+ color: var(--text-primary);
+ transition: all 0.2s ease;
+}
+
+.transfer-textarea:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px var(--primary-light);
+}
+
+.transfer-textarea::placeholder {
+ color: var(--text-tertiary);
+ font-size: 13px;
+}
+
+.format-hint-inline {
+ font-size: 12px;
+ color: var(--text-tertiary);
+ font-weight: 400;
+ margin-left: 8px;
+}
+
+.input-actions {
+ display: flex;
+ gap: var(--spacing-sm);
+ margin-top: var(--spacing-sm);
+}
+
+.upload-zone {
+ border: 2px dashed var(--border-color);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-xl);
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ background: var(--bg-color);
+}
+
+.upload-zone:hover {
+ border-color: var(--primary-color);
+ background: var(--primary-light);
+}
+
+.upload-zone i {
+ font-size: 48px;
+ color: var(--primary-color);
+ margin-bottom: var(--spacing-md);
+}
+
+.upload-zone h3 {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.upload-zone p {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: var(--spacing-md);
+}
+
+.format-hint {
+ font-size: 12px;
+ color: var(--text-secondary);
+ background: var(--card-bg);
+ padding: var(--spacing-sm);
+ border-radius: var(--radius-sm);
+ display: inline-block;
+ text-align: left;
+}
+
+/* ==================== 数据预览 ==================== */
+.data-preview {
+ padding-top: var(--spacing-md);
+ border-top: 1px solid var(--border-color);
+}
+
+.preview-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-md);
+}
+
+.preview-header h3 {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.count-badge {
+ background: var(--primary-color);
+ color: var(--text-primary);
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.preview-table {
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+thead {
+ background: var(--bg-color);
+ position: sticky;
+ top: 0;
+}
+
+th,
+td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+ font-size: 14px;
+}
+
+th {
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+tbody tr:hover {
+ background: var(--bg-color);
+}
+
+tbody tr:last-child td {
+ border-bottom: none;
+}
+
+/* ==================== 转账汇总 ==================== */
+.transfer-summary {
+ background: var(--bg-color);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-color);
+}
+
+.summary-row {
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-sm) 0;
+ border-bottom: 1px solid var(--border-color);
+ font-size: 14px;
+}
+
+.summary-row:last-child {
+ border-bottom: none;
+}
+
+.summary-row.total {
+ padding-top: var(--spacing-md);
+ border-top: 2px solid var(--border-color);
+ font-size: 16px;
+}
+
+.summary-row span {
+ color: var(--text-secondary);
+}
+
+.summary-row strong {
+ color: var(--text-primary);
+ font-weight: 600;
+}
+
+.summary-row.total strong {
+ color: var(--primary-color);
+}
+
+/* ==================== 高级设置 ==================== */
+.advanced-settings {
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-md);
+ background: var(--bg-color);
+}
+
+.advanced-settings summary {
+ cursor: pointer;
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ user-select: none;
+}
+
+.advanced-settings summary:hover {
+ color: var(--text-primary);
+}
+
+.advanced-settings[open] summary {
+ margin-bottom: var(--spacing-md);
+ padding-bottom: var(--spacing-md);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.settings-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--spacing-md);
+}
+
+.setting-item label {
+ display: block;
+ margin-bottom: var(--spacing-xs);
+ color: var(--text-secondary);
+ font-weight: 500;
+ font-size: 13px;
+}
+
+.input-with-unit {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.input-with-unit input {
+ flex: 1;
+}
+
+.input-with-unit span {
+ color: var(--text-secondary);
+ font-size: 13px;
+ font-weight: 500;
+}
+
+/* ==================== 操作按钮 ==================== */
+.action-buttons {
+ margin-top: var(--spacing-md);
+}
+
+/* ==================== 进度区域 ==================== */
+.progress-section {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-lg);
+}
+
+.progress-card,
+.log-card {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-color);
+}
+
+.progress-card h3,
+.log-card h3 {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: var(--spacing-md);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.progress-card h3 i,
+.log-card h3 i {
+ color: var(--primary-color);
+}
+
+.progress-bar-container {
+ margin-bottom: var(--spacing-md);
+}
+
+.progress-bar {
+ height: 8px;
+ background: var(--bg-color);
+ border-radius: 999px;
+ overflow: hidden;
+ margin-bottom: var(--spacing-sm);
+ border: 1px solid var(--border-color);
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--primary-color);
+ transition: width 0.3s ease;
+ border-radius: 999px;
+}
+
+.progress-text {
+ display: flex;
+ justify-content: space-between;
+ color: var(--text-secondary);
+ font-size: 13px;
+ font-weight: 500;
+}
+
+.progress-stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--spacing-md);
+}
+
+.stat {
+ background: var(--bg-color);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ border: 1px solid var(--border-color);
+}
+
+.stat i {
+ font-size: 24px;
+}
+
+.stat.success i {
+ color: var(--success-color);
+}
+
+.stat.failed i {
+ color: var(--error-color);
+}
+
+.stat.pending i {
+ color: var(--warning-color);
+}
+
+.stat span {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.stat strong {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+/* ==================== 日志 ==================== */
+.log-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-md);
+}
+
+.log-header h3 {
+ margin-bottom: 0;
+}
+
+.log-content {
+ background: var(--bg-color);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-md);
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+}
+
+.log-empty {
+ text-align: center;
+ padding: var(--spacing-xl);
+ color: var(--text-tertiary);
+}
+
+.log-empty i {
+ font-size: 36px;
+ margin-bottom: var(--spacing-sm);
+ opacity: 0.3;
+}
+
+.log-item {
+ padding: var(--spacing-sm);
+ margin-bottom: var(--spacing-xs);
+ background: var(--card-bg);
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--text-tertiary);
+ font-size: 13px;
+}
+
+.log-item.success {
+ border-left-color: var(--success-color);
+ background: #F0FDF4;
+}
+
+.log-item.error {
+ border-left-color: var(--error-color);
+ background: #FEF2F2;
+}
+
+.log-item.info {
+ border-left-color: var(--info-color);
+ background: #EFF6FF;
+}
+
+.log-item.warning {
+ border-left-color: var(--warning-color);
+ background: #FFFBEB;
+}
+
+.log-time {
+ font-size: 11px;
+ color: var(--text-tertiary);
+ margin-bottom: 4px;
+}
+
+.log-message {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ color: var(--text-primary);
+}
+
+/* ==================== 底部 ==================== */
+.footer {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-md);
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 13px;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-color);
+}
+
+.footer p {
+ margin-bottom: var(--spacing-xs);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-xs);
+}
+
+.footer p:last-child {
+ margin-bottom: 0;
+}
+
+/* ==================== 模态框 ==================== */
+.modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-content {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ width: 500px;
+ max-width: 90%;
+ box-shadow: var(--shadow-lg);
+ border: 1px solid var(--border-color);
+}
+
+.modal-header {
+ padding: var(--spacing-lg);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.modal-header h3 i {
+ color: var(--warning-color);
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: var(--text-tertiary);
+ transition: all 0.2s;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+}
+
+.modal-close:hover {
+ color: var(--text-primary);
+ background: var(--bg-color);
+}
+
+.modal-body {
+ padding: var(--spacing-lg);
+}
+
+.modal-body > p {
+ margin-bottom: var(--spacing-md);
+ color: var(--text-secondary);
+}
+
+.modal-footer {
+ padding: var(--spacing-lg);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--spacing-sm);
+}
+
+.confirm-details {
+ background: var(--bg-color);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ margin: var(--spacing-md) 0;
+ border: 1px solid var(--border-color);
+}
+
+.confirm-details p {
+ margin-bottom: var(--spacing-sm);
+ display: flex;
+ justify-content: space-between;
+ font-size: 14px;
+}
+
+.confirm-details p:last-child {
+ margin-bottom: 0;
+}
+
+.warning {
+ color: var(--error-color);
+ font-weight: 600;
+ margin: var(--spacing-md) 0;
+ padding: var(--spacing-sm);
+ background: #FEF2F2;
+ border-radius: var(--radius-sm);
+ border-left: 3px solid var(--error-color);
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ cursor: pointer;
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.checkbox-label input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+/* ==================== 响应式 ==================== */
+@media (max-width: 768px) {
+ body {
+ padding: var(--spacing-md);
+ }
+
+ .container {
+ max-width: 100%;
+ }
+
+ .header {
+ flex-direction: column;
+ gap: var(--spacing-md);
+ padding: var(--spacing-md);
+ }
+
+ .wallet-connected {
+ width: 100%;
+ }
+
+ .transfer-type-selector {
+ grid-template-columns: 1fr;
+ }
+
+ .info-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .settings-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .progress-stats {
+ grid-template-columns: 1fr;
+ }
+
+ .step-card {
+ padding: var(--spacing-md);
+ }
+}
+
+/* ==================== 滚动条 ==================== */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-color);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 999px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-tertiary);
+}
+
+/* ==================== 动画 ==================== */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.step-card,
+.progress-card,
+.log-card {
+ animation: fadeIn 0.3s ease;
+}
+
+/* ==================== 历史页面样式 ==================== */
+.history-toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ gap: var(--spacing-md);
+ flex-wrap: wrap;
+}
+
+.filter-group {
+ display: flex;
+ gap: var(--spacing-md);
+ flex: 1;
+ flex-wrap: wrap;
+}
+
+.filter-item {
+ flex: 1;
+ min-width: 150px;
+}
+
+.filter-item label {
+ display: block;
+ margin-bottom: var(--spacing-xs);
+ color: var(--text-secondary);
+ font-weight: 500;
+ font-size: 13px;
+}
+
+.action-group {
+ display: flex;
+ gap: var(--spacing-sm);
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-lg);
+}
+
+.stat-card {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+}
+
+.stat-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+ flex-shrink: 0;
+}
+
+.stat-icon.success {
+ background: #F0FDF4;
+ color: var(--success-color);
+}
+
+.stat-icon.failed {
+ background: #FEF2F2;
+ color: var(--error-color);
+}
+
+.stat-icon.pending {
+ background: #FFFBEB;
+ color: var(--warning-color);
+}
+
+.stat-icon.total {
+ background: var(--primary-light);
+ color: var(--primary-color);
+}
+
+.stat-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.stat-label {
+ font-size: 12px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.stat-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.history-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-md);
+}
+
+.history-header h3 {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ margin: 0;
+}
+
+.record-count {
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.history-table-container {
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+}
+
+.history-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.history-table thead {
+ background: var(--bg-color);
+}
+
+.history-table th {
+ padding: 12px;
+ text-align: left;
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.history-table td {
+ padding: 12px;
+ font-size: 14px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.history-table tbody tr:hover {
+ background: var(--bg-color);
+}
+
+.history-table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+.empty-state td {
+ padding: var(--spacing-xl) !important;
+}
+
+.empty-content {
+ text-align: center;
+ color: var(--text-tertiary);
+}
+
+.empty-content i {
+ font-size: 48px;
+ margin-bottom: var(--spacing-sm);
+ opacity: 0.3;
+}
+
+.empty-content p {
+ margin: 0;
+ font-size: 14px;
+}
+
+.type-badge {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: var(--radius-sm);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.type-badge.bnb {
+ background: var(--primary-light);
+ color: var(--primary-dark);
+}
+
+.type-badge.token {
+ background: #EFF6FF;
+ color: var(--info-color);
+}
+
+.address-cell {
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+}
+
+.status-badge {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: var(--radius-sm);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.status-badge.success {
+ background: #F0FDF4;
+ color: var(--success-color);
+}
+
+.status-badge.failed {
+ background: #FEF2F2;
+ color: var(--error-color);
+}
+
+.status-badge.pending {
+ background: #FFFBEB;
+ color: var(--warning-color);
+}
+
+.tx-link {
+ color: var(--info-color);
+ text-decoration: none;
+ font-family: 'SF Mono', Monaco, monospace;
+ font-size: 13px;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.tx-link:hover {
+ text-decoration: underline;
+}
+
+.detail-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--spacing-md);
+}
+
+.detail-item {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.detail-item .label {
+ font-size: 12px;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.detail-item .value {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+ word-break: break-all;
+}
+
+/* 历史页面响应式 */
+@media (max-width: 768px) {
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .history-toolbar {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .filter-group {
+ flex-direction: column;
+ }
+
+ .filter-item {
+ min-width: 100%;
+ }
+
+ .action-group {
+ width: 100%;
+ }
+
+ .action-group button {
+ flex: 1;
+ }
+
+ .history-table {
+ font-size: 12px;
+ }
+
+ .history-table th,
+ .history-table td {
+ padding: 8px;
+ }
+
+ .detail-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/utils.js b/utils.js
new file mode 100644
index 0000000..da68378
--- /dev/null
+++ b/utils.js
@@ -0,0 +1,117 @@
+class TransferUtils {
+ static parseCSV(csvText) {
+ const lines = csvText.split('\n');
+ const transfers = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ // 支持逗号分隔或制表符分隔
+ const parts = line.includes('\t') ? line.split('\t') : line.split(',');
+
+ if (parts.length >= 2) {
+ const address = parts[0].trim();
+ const amount = parts[1].trim();
+ const note = parts[2] ? parts[2].trim() : '';
+
+ // 基本验证
+ if (address && amount) {
+ transfers.push({
+ address,
+ amount: parseFloat(amount),
+ note
+ });
+ }
+ }
+ }
+
+ return transfers;
+ }
+
+ static formatAddress(address) {
+ if (!address || address.length < 10) return address;
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+ }
+
+ static formatNumber(num, decimals = 4) {
+ if (isNaN(num)) return '0';
+ return parseFloat(num.toFixed(decimals)).toString();
+ }
+
+ static generateLogMessage(type, message) {
+ const time = new Date().toLocaleTimeString();
+ const types = {
+ success: { icon: '✅', class: 'success' },
+ error: { icon: '❌', class: 'error' },
+ info: { icon: 'ℹ️', class: 'info' },
+ warning: { icon: '⚠️', class: 'warning' }
+ };
+
+ const typeInfo = types[type] || types.info;
+
+ return {
+ time,
+ type: typeInfo.class,
+ message: `${typeInfo.icon} ${message}`,
+ html: `
+
+
${time}
+
${typeInfo.icon} ${message}
+
+ `
+ };
+ }
+
+ static exportToCSV(data, filename = 'transfer-results.csv') {
+ const headers = ['序号', '收款地址', '金额(BNB)', '状态', '交易哈希', '备注'];
+ const rows = data.map(item => [
+ item.index + 1,
+ item.to,
+ item.amount,
+ item.success ? '成功' : '失败',
+ item.hash || '',
+ item.error || ''
+ ]);
+
+ const csvContent = [headers, ...rows]
+ .map(row => row.map(cell => `"${cell}"`).join(','))
+ .join('\n');
+
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+
+ if (navigator.msSaveBlob) {
+ navigator.msSaveBlob(blob, filename);
+ } else {
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ link.style.display = 'none';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+ }
+
+ static validateEthereumAddress(address) {
+ try {
+ return ethers.utils.isAddress(address);
+ } catch {
+ return false;
+ }
+ }
+
+ static async checkBalanceSufficient(provider, address, requiredAmount) {
+ try {
+ const balance = await provider.getBalance(address);
+ const required = ethers.utils.parseEther(requiredAmount.toString());
+ return balance.gte(required);
+ } catch (error) {
+ console.error('检查余额失败:', error);
+ return false;
+ }
+ }
+}
+
+// 导出工具类
+window.TransferUtils = TransferUtils;
\ No newline at end of file