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开发以及区块链工具开发。 +

+
+ + +
+

联系方式

+
+ +
+
+ +
+
+
微信号
+
aaronlzhou
+
+ +
+ + +
+
+ +
+
+
电子邮箱
+
aaron.l.zhou@gmail.com
+
+ +
+
+
+ + + +
+ + + + 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 @@ + + + + + + 转账历史 | 区块链龙哥 出品 + + + + + +
+ +
+
+

转账历史

+

查看所有批量转账记录

+
+ +
+ + +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+ 成功转账 + 0 +
+
+
+
+ +
+
+ 失败转账 + 0 +
+
+
+
+ +
+
+ 待确认 + 0 +
+
+
+
+ +
+
+ 总转账数 + 0 +
+
+
+ + +
+
+

转账记录

+ 0 条记录 +
+
+ + + + + + + + + + + + + + + + + + +
时间类型收款地址金额状态交易哈希备注操作
+
+ +

暂无转账记录

+
+
+
+
+
+ + + +
+ + + + + + + + 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批量转账工具 | 区块链龙哥 出品 + + + + + +
+ +
+
+

BSC批量转账

+

简单、快速、安全的批量转账工具

+
+ + 联系开发者 + + + 转账历史 + +
+
+
+ + +
+
+ + +
+ +
+
+ 1 +

选择转账类型

+
+
+
+ + +
+ + + +
+
+ + +
+
+ 2 +

输入转账数据

+
+
+
+ + +
+ + +
+
+ + + +
+
+ + +
+
+ 3 +

确认并开始转账

+
+
+ +
+
+ 转账数量 + 0 +
+
+ 转账总额 + 0 BNB +
+
+ 预计Gas费 + 0 BNB +
+
+ 总计费用 + 0 BNB +
+
+ + +
+ + 高级设置 + +
+
+ + +
+
+ + +
+
+ +
+ + Gwei +
+
+
+
+ + +
+ + +
+
+
+ + + +
+ + + +
+ + + + + + + + + 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