From 2ff09f91b0ff126dd1a56da5465a253d82eded1e Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sat, 24 Jan 2026 22:22:11 +0800 Subject: [PATCH] first commit --- README.md | 142 +++++ app.js | 696 +++++++++++++++++++++++ batchTransfer.js | 435 +++++++++++++++ developer.html | 367 ++++++++++++ example.csv | 3 + history.html | 202 +++++++ history.js | 322 +++++++++++ index.html | 308 ++++++++++ style.css | 1394 ++++++++++++++++++++++++++++++++++++++++++++++ utils.js | 117 ++++ 10 files changed, 3986 insertions(+) create mode 100644 README.md create mode 100644 app.js create mode 100644 batchTransfer.js create mode 100644 developer.html create mode 100644 example.csv create mode 100644 history.html create mode 100644 history.js create mode 100644 index.html create mode 100644 style.css create mode 100644 utils.js 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