diff --git a/frontend/src/App.css b/frontend/src/App.css index 1983fe0..83b068d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -647,3 +647,180 @@ body { color: #fff; font-weight: 500; } + +/* Toast notifications */ +.toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + cursor: pointer; + animation: slideIn 0.3s ease; + max-width: 360px; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.toast-success { + background: #e8f5e9; + color: #2e7d32; + border-left: 4px solid #4caf50; +} + +.toast-error { + background: #ffebee; + color: #c62828; + border-left: 4px solid #f44336; +} + +.toast-warning { + background: #fff3e0; + color: #e65100; + border-left: 4px solid #ff9800; +} + +.toast-info { + background: #e3f2fd; + color: #1565c0; + border-left: 4px solid #2196f3; +} + +.toast-icon { + font-size: 16px; + font-weight: bold; +} + +.toast-message { + font-size: 14px; + flex: 1; +} + +/* Transaction History */ +.tx-history { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid #e0e0e0; +} + +.tx-history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.tx-history-header h3 { + margin: 0; + font-size: 16px; +} + +.tx-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tx-item { + background: #f9f9f9; + padding: 10px 14px; + border-radius: 6px; + border-left: 3px solid #ddd; +} + +.tx-item.tx-success { + border-left-color: #4caf50; +} + +.tx-item.tx-failed { + border-left-color: #f44336; +} + +.tx-item.tx-pending { + border-left-color: #ff9800; +} + +.tx-info { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 4px; +} + +.tx-type { + font-weight: 600; + font-size: 13px; + color: #333; +} + +.tx-amount { + font-size: 13px; + color: #666; +} + +.tx-time { + font-size: 11px; + color: #999; + margin-left: auto; +} + +.tx-hash { + display: flex; + align-items: center; + gap: 8px; +} + +.tx-hash a { + font-family: monospace; + font-size: 12px; + color: #1976d2; + text-decoration: none; +} + +.tx-hash a:hover { + text-decoration: underline; +} + +.tx-status { + font-size: 12px; + font-weight: bold; +} + +.tx-status.success { + color: #4caf50; +} + +.tx-status.failed { + color: #f44336; +} + +.tx-status.pending { + color: #ff9800; +} + +.tx-error { + font-size: 12px; + color: #c62828; + margin-top: 4px; + word-break: break-all; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9437a50..b82e571 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,8 @@ import { LanguageSwitch } from './components/LanguageSwitch' import { WUSDPanel } from './components/WUSDPanel' import { VaultPanel } from './components/VaultPanel' import { FactoryPanel } from './components/FactoryPanel' +import { ToastProvider } from './components/Toast' +import { TransactionProvider } from './context/TransactionContext' import './App.css' type Tab = 'wusd' | 'vault' | 'factory' @@ -65,7 +67,11 @@ function App() { return ( - + + + + + ) diff --git a/frontend/src/components/FactoryPanel.tsx b/frontend/src/components/FactoryPanel.tsx index 96c2e14..021244b 100644 --- a/frontend/src/components/FactoryPanel.tsx +++ b/frontend/src/components/FactoryPanel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits } from 'viem' -import { CONTRACTS, FACTORY_ABI, VAULT_ABI } from '../config/contracts' +import { CONTRACTS, FACTORY_ABI } from '../config/contracts' export function FactoryPanel() { const { t } = useTranslation() diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx new file mode 100644 index 0000000..e8a3c22 --- /dev/null +++ b/frontend/src/components/Toast.tsx @@ -0,0 +1,70 @@ +import { useState, useEffect, createContext, useContext } from 'react' +import type { ReactNode } from 'react' + +export type ToastType = 'success' | 'error' | 'info' | 'warning' + +interface Toast { + id: string + type: ToastType + message: string + duration?: number +} + +interface ToastContextType { + showToast: (type: ToastType, message: string, duration?: number) => void +} + +const ToastContext = createContext(null) + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]) + + const showToast = (type: ToastType, message: string, duration = 4000) => { + const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` + setToasts(prev => [...prev, { id, type, message, duration }]) + } + + const removeToast = (id: string) => { + setToasts(prev => prev.filter(t => t.id !== id)) + } + + return ( + + {children} +
+ {toasts.map(toast => ( + + ))} +
+
+ ) +} + +function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) { + useEffect(() => { + const timer = setTimeout(() => { + onRemove(toast.id) + }, toast.duration || 4000) + return () => clearTimeout(timer) + }, [toast.id, toast.duration, onRemove]) + + return ( +
onRemove(toast.id)}> + + {toast.type === 'success' && '✓'} + {toast.type === 'error' && '✗'} + {toast.type === 'warning' && '⚠'} + {toast.type === 'info' && 'ℹ'} + + {toast.message} +
+ ) +} + +export function useToast() { + const context = useContext(ToastContext) + if (!context) { + throw new Error('useToast must be used within ToastProvider') + } + return context +} diff --git a/frontend/src/components/TransactionHistory.tsx b/frontend/src/components/TransactionHistory.tsx new file mode 100644 index 0000000..ec54fa6 --- /dev/null +++ b/frontend/src/components/TransactionHistory.tsx @@ -0,0 +1,88 @@ +import { useTranslation } from 'react-i18next' +import type { TransactionRecord } from '../hooks/useTransactionHistory' + +interface Props { + transactions: TransactionRecord[] + onClear: () => void +} + +export function TransactionHistory({ transactions, onClear }: Props) { + const { t } = useTranslation() + + const getTypeLabel = (type: string) => { + const labels: Record = { + mint: 'Mint WUSD', + burn: 'Burn WUSD', + buy: 'Buy YT', + sell: 'Sell YT', + approve: 'Approve', + create_vault: 'Create Vault', + update_price: 'Update Price', + test: 'Test', + } + return labels[type] || type + } + + const getStatusClass = (status: string) => { + switch (status) { + case 'success': return 'tx-success' + case 'failed': return 'tx-failed' + default: return 'tx-pending' + } + } + + const formatTime = (timestamp: number) => { + const date = new Date(timestamp) + return date.toLocaleString() + } + + const shortenHash = (hash: string) => { + return `${hash.slice(0, 8)}...${hash.slice(-6)}` + } + + if (transactions.length === 0) { + return ( +
+
+

{t('history.title')}

+
+

{t('history.empty')}

+
+ ) + } + + return ( +
+
+

{t('history.title')}

+ +
+
+ {transactions.slice(0, 10).map((tx) => ( +
+
+ {getTypeLabel(tx.type)} + {tx.amount && {tx.amount} {tx.token}} + {formatTime(tx.timestamp)} +
+
+ + {shortenHash(tx.hash)} + + + {tx.status === 'success' ? '✓' : tx.status === 'failed' ? '✗' : '...'} + +
+ {tx.error &&
{tx.error}
} +
+ ))} +
+
+ ) +} diff --git a/frontend/src/components/VaultPanel.tsx b/frontend/src/components/VaultPanel.tsx index 19b285c..f1b1ea2 100644 --- a/frontend/src/components/VaultPanel.tsx +++ b/frontend/src/components/VaultPanel.tsx @@ -1,8 +1,12 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits, maxUint256 } from 'viem' import { CONTRACTS, VAULT_ABI, WUSD_ABI, FACTORY_ABI } from '../config/contracts' +import { useTransactions } from '../context/TransactionContext' +import type { TransactionType } from '../context/TransactionContext' +import { useToast } from './Toast' +import { TransactionHistory } from './TransactionHistory' const VAULTS = [ { name: 'YT-A', address: CONTRACTS.VAULTS.YT_A }, @@ -13,12 +17,15 @@ const VAULTS = [ export function VaultPanel() { const { t } = useTranslation() const { address, isConnected } = useAccount() + const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions() + const { showToast } = useToast() const [selectedVault, setSelectedVault] = useState(VAULTS[0]) const [buyAmount, setBuyAmount] = useState('') const [sellAmount, setSellAmount] = useState('') const [activeTab, setActiveTab] = useState<'buy' | 'sell'>('buy') const [showBoundaryTest, setShowBoundaryTest] = useState(false) const [testResult, setTestResult] = useState<{ type: 'success' | 'error' | 'pending', msg: string } | null>(null) + const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null) const { data: vaultInfo, refetch: refetchVaultInfo } = useReadContract({ address: selectedVault.address as `0x${string}`, @@ -98,14 +105,28 @@ export function VaultPanel() { functionName: 'owner', }) - const { writeContract, data: hash, isPending, reset } = useWriteContract() + const { writeContract, data: hash, isPending, reset, error: writeError } = useWriteContract() - const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ hash, }) + // 处理交易提交 + useEffect(() => { + if (hash && pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' }) + showToast('info', t('toast.txSubmitted')) + } + }, [hash]) + + // 处理交易成功 useEffect(() => { if (isSuccess) { + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'success' }) + showToast('success', t('toast.txSuccess')) + pendingTxRef.current = null + } refetchVaultInfo() refetchYtBalance() refetchWusdBalance() @@ -116,8 +137,61 @@ export function VaultPanel() { } }, [isSuccess]) + // 处理交易失败 + useEffect(() => { + if (isError && pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { + status: 'failed', + error: txError?.message || 'Transaction failed' + }) + showToast('error', t('toast.txFailed')) + pendingTxRef.current = null + } + }, [isError]) + + // 处理写入错误 + useEffect(() => { + if (writeError) { + const errorMsg = parseError(writeError) + showToast('error', errorMsg) + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg }) + pendingTxRef.current = null + } + } + }, [writeError]) + + // 解析错误信息 + const parseError = (error: any): string => { + const msg = error?.message || error?.toString() || 'Unknown error' + if (msg.includes('User rejected') || msg.includes('user rejected')) { + return t('toast.userRejected') + } + if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance')) { + return t('toast.insufficientBalance') + } + // 提取合约错误 + const match = msg.match(/error[:\s]+(\w+)/i) + if (match) return match[1] + return msg.slice(0, 100) + } + + // 记录交易 + const recordTx = (type: TransactionType, amount?: string, token?: string) => { + const id = addTransaction({ + type, + hash: '', + status: 'pending', + amount, + token, + vault: selectedVault.name, + }) + pendingTxRef.current = { id, type, amount } + } + const handleApprove = async () => { if (!buyAmount) return + recordTx('approve', buyAmount, 'WUSD') writeContract({ address: CONTRACTS.WUSD, abi: WUSD_ABI, @@ -128,6 +202,7 @@ export function VaultPanel() { const handleBuy = async () => { if (!buyAmount) return + recordTx('buy', buyAmount, 'WUSD') writeContract({ address: selectedVault.address as `0x${string}`, abi: VAULT_ABI, @@ -138,6 +213,7 @@ export function VaultPanel() { const handleSell = async () => { if (!sellAmount) return + recordTx('sell', sellAmount, 'YT') writeContract({ address: selectedVault.address as `0x${string}`, abi: VAULT_ABI, @@ -499,6 +575,9 @@ export function VaultPanel() { )} + + {/* 交易历史 */} + ) } diff --git a/frontend/src/components/WUSDPanel.tsx b/frontend/src/components/WUSDPanel.tsx index 0689621..4eefb51 100644 --- a/frontend/src/components/WUSDPanel.tsx +++ b/frontend/src/components/WUSDPanel.tsx @@ -1,14 +1,21 @@ -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits } from 'viem' import { CONTRACTS, WUSD_ABI } from '../config/contracts' +import { useTransactions } from '../context/TransactionContext' +import type { TransactionType } from '../context/TransactionContext' +import { useToast } from './Toast' +import { TransactionHistory } from './TransactionHistory' export function WUSDPanel() { const { t } = useTranslation() const { address, isConnected } = useAccount() + const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions() + const { showToast } = useToast() const [mintAmount, setMintAmount] = useState('') const [showBoundaryTest, setShowBoundaryTest] = useState(false) + const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null) const { data: balance, refetch: refetchBalance } = useReadContract({ address: CONTRACTS.WUSD, @@ -29,67 +36,126 @@ export function WUSDPanel() { functionName: 'decimals', }) - const { writeContract, data: hash, isPending } = useWriteContract() + const { writeContract, data: hash, isPending, error: writeError } = useWriteContract() - const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ + const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ hash, }) + // 处理交易提交 + useEffect(() => { + if (hash && pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' }) + showToast('info', t('toast.txSubmitted')) + } + }, [hash]) + + // 处理交易成功 + useEffect(() => { + if (isSuccess) { + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'success' }) + showToast('success', t('toast.txSuccess')) + pendingTxRef.current = null + } + refetchBalance() + setMintAmount('') + } + }, [isSuccess]) + + // 处理交易失败 + useEffect(() => { + if (isError && pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { + status: 'failed', + error: txError?.message || 'Transaction failed' + }) + showToast('error', t('toast.txFailed')) + pendingTxRef.current = null + } + }, [isError]) + + // 处理写入错误 + useEffect(() => { + if (writeError) { + const errorMsg = parseError(writeError) + showToast('error', errorMsg) + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg }) + pendingTxRef.current = null + } + } + }, [writeError]) + + const parseError = (error: any): string => { + const msg = error?.message || error?.toString() || 'Unknown error' + if (msg.includes('User rejected') || msg.includes('user rejected')) { + return t('toast.userRejected') + } + if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance')) { + return t('toast.insufficientBalance') + } + const match = msg.match(/error[:\s]+(\w+)/i) + if (match) return match[1] + return msg.slice(0, 100) + } + + const recordTx = (type: TransactionType, amount?: string, token?: string) => { + const id = addTransaction({ + type, + hash: '', + status: 'pending', + amount, + token, + }) + pendingTxRef.current = { id, type, amount } + } + const handleMint = async () => { if (!address || !mintAmount || !decimals) return - - try { - writeContract({ - address: CONTRACTS.WUSD, - abi: WUSD_ABI, - functionName: 'mint', - args: [address, parseUnits(mintAmount, decimals)], - }) - } catch (error) { - console.error('Mint error:', error) - } + recordTx('mint', mintAmount, 'WUSD') + writeContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'mint', + args: [address, parseUnits(mintAmount, decimals)], + }) } // 边界测试函数 const runBoundaryTest = (testType: string) => { if (!address || !decimals) return - try { - switch (testType) { - case 'mint_zero': - writeContract({ - address: CONTRACTS.WUSD, - abi: WUSD_ABI, - functionName: 'mint', - args: [address, BigInt(0)], - }) - break - case 'burn_exceed': - const exceedAmount = balance ? balance + parseUnits('999999', decimals) : parseUnits('999999999', decimals) - writeContract({ - address: CONTRACTS.WUSD, - abi: WUSD_ABI, - functionName: 'burn', - args: [address, exceedAmount], - }) - break - case 'mint_10000': - writeContract({ - address: CONTRACTS.WUSD, - abi: WUSD_ABI, - functionName: 'mint', - args: [address, parseUnits('10000', decimals)], - }) - break - } - } catch (err) { - console.error('Test error:', err) + recordTx('test', undefined, 'WUSD') + switch (testType) { + case 'mint_zero': + writeContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'mint', + args: [address, BigInt(0)], + }) + break + case 'burn_exceed': + const exceedAmount = balance ? balance + parseUnits('999999', decimals) : parseUnits('999999999', decimals) + writeContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'burn', + args: [address, exceedAmount], + }) + break + case 'mint_10000': + pendingTxRef.current!.amount = '10000' + writeContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'mint', + args: [address, parseUnits('10000', decimals)], + }) + break } } - if (isSuccess) { - refetchBalance() - } - if (!isConnected) { return (
@@ -134,10 +200,6 @@ export function WUSDPanel() { {isPending ? t('wusd.confirming') : isConfirming ? t('wusd.minting') : t('wusd.mint')} - {isSuccess && ( -

{t('wusd.mintSuccess')}

- )} - {/* 边界测试区域 */}
setShowBoundaryTest(!showBoundaryTest)} style={{ cursor: 'pointer' }}> @@ -180,6 +242,9 @@ export function WUSDPanel() { )}
+ + {/* 交易历史 */} +
) } diff --git a/frontend/src/context/TransactionContext.tsx b/frontend/src/context/TransactionContext.tsx new file mode 100644 index 0000000..052b04d --- /dev/null +++ b/frontend/src/context/TransactionContext.tsx @@ -0,0 +1,33 @@ +import { createContext, useContext } from 'react' +import type { ReactNode } from 'react' +import { useTransactionHistory } from '../hooks/useTransactionHistory' +import type { TransactionRecord, TransactionType } from '../hooks/useTransactionHistory' + +interface TransactionContextType { + transactions: TransactionRecord[] + addTransaction: (tx: Omit) => string + updateTransaction: (id: string, updates: Partial) => void + clearHistory: () => void +} + +const TransactionContext = createContext(null) + +export function TransactionProvider({ children }: { children: ReactNode }) { + const history = useTransactionHistory() + + return ( + + {children} + + ) +} + +export function useTransactions() { + const context = useContext(TransactionContext) + if (!context) { + throw new Error('useTransactions must be used within TransactionProvider') + } + return context +} + +export type { TransactionType, TransactionRecord } diff --git a/frontend/src/hooks/useTransactionHistory.ts b/frontend/src/hooks/useTransactionHistory.ts new file mode 100644 index 0000000..6a2ed33 --- /dev/null +++ b/frontend/src/hooks/useTransactionHistory.ts @@ -0,0 +1,82 @@ +import { useState, useEffect } from 'react' + +export type TransactionType = 'mint' | 'burn' | 'buy' | 'sell' | 'approve' | 'create_vault' | 'update_price' | 'test' + +export interface TransactionRecord { + id: string + type: TransactionType + hash: string + timestamp: number + status: 'pending' | 'success' | 'failed' + amount?: string + token?: string + vault?: string + error?: string +} + +const STORAGE_KEY = 'yt_asset_tx_history' +const MAX_RECORDS = 50 + +export function useTransactionHistory() { + const [transactions, setTransactions] = useState([]) + + // 从 localStorage 加载历史记录 + useEffect(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + setTransactions(JSON.parse(stored)) + } + } catch (e) { + console.error('Failed to load transaction history:', e) + } + }, []) + + // 保存到 localStorage + const saveToStorage = (records: TransactionRecord[]) => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(records.slice(0, MAX_RECORDS))) + } catch (e) { + console.error('Failed to save transaction history:', e) + } + } + + // 添加新交易 + const addTransaction = (tx: Omit) => { + const newTx: TransactionRecord = { + ...tx, + id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: Date.now(), + } + setTransactions(prev => { + const updated = [newTx, ...prev].slice(0, MAX_RECORDS) + saveToStorage(updated) + return updated + }) + return newTx.id + } + + // 更新交易状态 + const updateTransaction = (id: string, updates: Partial) => { + setTransactions(prev => { + const updated = prev.map(tx => + tx.id === id ? { ...tx, ...updates } : tx + ) + saveToStorage(updated) + return updated + }) + } + + // 清空历史记录 + const clearHistory = () => { + setTransactions([]) + localStorage.removeItem(STORAGE_KEY) + } + + return { + transactions, + addTransaction, + updateTransaction, + clearHistory, + } +} diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 3f05e8f..134d6ae 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -88,6 +88,22 @@ "en": "English", "zh": "Chinese" }, + "history": { + "title": "Transaction History", + "empty": "No transactions yet", + "clear": "Clear", + "viewMore": "View More" + }, + "toast": { + "txSubmitted": "Transaction submitted", + "txSuccess": "Transaction successful", + "txFailed": "Transaction failed", + "copySuccess": "Copied to clipboard", + "walletError": "Wallet error", + "networkError": "Network error", + "insufficientBalance": "Insufficient balance", + "userRejected": "User rejected the transaction" + }, "test": { "title": "Boundary Test", "currentStatus": "Current Status", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index dea7637..888c4d7 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -88,6 +88,22 @@ "en": "英文", "zh": "中文" }, + "history": { + "title": "交易记录", + "empty": "暂无交易记录", + "clear": "清空", + "viewMore": "查看更多" + }, + "toast": { + "txSubmitted": "交易已提交", + "txSuccess": "交易成功", + "txFailed": "交易失败", + "copySuccess": "已复制到剪贴板", + "walletError": "钱包错误", + "networkError": "网络错误", + "insufficientBalance": "余额不足", + "userRejected": "用户取消了交易" + }, "test": { "title": "边界测试", "currentStatus": "当前状态", diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 807024a..3f0fa5a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,5 +9,26 @@ export default defineConfig({ host: '0.0.0.0', strictPort: true, allowedHosts: ['maxfight.vip', 'localhost', '127.0.0.1'], - } + }, + build: { + rollupOptions: { + output: { + manualChunks: { + // React 相关 + 'react-vendor': ['react', 'react-dom'], + // Web3 相关 + 'web3-vendor': ['wagmi', 'viem', '@tanstack/react-query'], + // WalletConnect 相关 + 'walletconnect': [ + '@web3modal/wagmi', + '@walletconnect/ethereum-provider', + ], + // i18n + 'i18n': ['react-i18next', 'i18next', 'i18next-browser-languagedetector'], + }, + }, + }, + // 提高 chunk 大小警告阈值 + chunkSizeWarningLimit: 600, + }, })