feat: 添加交易历史、错误提示和打包优化

1. 交易历史记录功能
   - 新增 useTransactionHistory hook 管理交易记录
   - 新增 TransactionHistory 组件显示历史
   - 交易记录保存到 localStorage

2. 错误处理和用户提示
   - 新增 Toast 通知组件
   - 交易提交/成功/失败时显示提示
   - 解析并显示友好的错误信息

3. 打包优化
   - 配置代码分割 (manualChunks)
   - 分离 react/web3/walletconnect/i18n
   - 提高 chunk 大小警告阈值

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 17:35:36 +00:00
parent 79fede5cb0
commit 553ff58c4d
12 changed files with 711 additions and 58 deletions

View File

@@ -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;
}

View File

@@ -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 (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<AppContent />
<ToastProvider>
<TransactionProvider>
<AppContent />
</TransactionProvider>
</ToastProvider>
</QueryClientProvider>
</WagmiProvider>
)

View File

@@ -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()

View File

@@ -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<ToastContextType | null>(null)
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([])
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 (
<ToastContext.Provider value={{ showToast }}>
{children}
<div className="toast-container">
{toasts.map(toast => (
<ToastItem key={toast.id} toast={toast} onRemove={removeToast} />
))}
</div>
</ToastContext.Provider>
)
}
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 (
<div className={`toast toast-${toast.type}`} onClick={() => onRemove(toast.id)}>
<span className="toast-icon">
{toast.type === 'success' && '✓'}
{toast.type === 'error' && '✗'}
{toast.type === 'warning' && '⚠'}
{toast.type === 'info' && ''}
</span>
<span className="toast-message">{toast.message}</span>
</div>
)
}
export function useToast() {
const context = useContext(ToastContext)
if (!context) {
throw new Error('useToast must be used within ToastProvider')
}
return context
}

View File

@@ -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<string, string> = {
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 (
<div className="tx-history">
<div className="tx-history-header">
<h3>{t('history.title')}</h3>
</div>
<p className="text-muted">{t('history.empty')}</p>
</div>
)
}
return (
<div className="tx-history">
<div className="tx-history-header">
<h3>{t('history.title')}</h3>
<button onClick={onClear} className="btn btn-sm btn-secondary">
{t('history.clear')}
</button>
</div>
<div className="tx-list">
{transactions.slice(0, 10).map((tx) => (
<div key={tx.id} className={`tx-item ${getStatusClass(tx.status)}`}>
<div className="tx-info">
<span className="tx-type">{getTypeLabel(tx.type)}</span>
{tx.amount && <span className="tx-amount">{tx.amount} {tx.token}</span>}
<span className="tx-time">{formatTime(tx.timestamp)}</span>
</div>
<div className="tx-hash">
<a
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{shortenHash(tx.hash)}
</a>
<span className={`tx-status ${tx.status}`}>
{tx.status === 'success' ? '✓' : tx.status === 'failed' ? '✗' : '...'}
</span>
</div>
{tx.error && <div className="tx-error">{tx.error}</div>}
</div>
))}
</div>
</div>
)
}

View File

@@ -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() {
</>
)}
</div>
{/* 交易历史 */}
<TransactionHistory transactions={transactions} onClear={clearHistory} />
</div>
)
}

View File

@@ -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 (
<div className="panel">
@@ -134,10 +200,6 @@ export function WUSDPanel() {
{isPending ? t('wusd.confirming') : isConfirming ? t('wusd.minting') : t('wusd.mint')}
</button>
{isSuccess && (
<p className="success-text">{t('wusd.mintSuccess')}</p>
)}
{/* 边界测试区域 */}
<div className="section">
<div className="section-header" onClick={() => setShowBoundaryTest(!showBoundaryTest)} style={{ cursor: 'pointer' }}>
@@ -180,6 +242,9 @@ export function WUSDPanel() {
</>
)}
</div>
{/* 交易历史 */}
<TransactionHistory transactions={transactions} onClear={clearHistory} />
</div>
)
}

View File

@@ -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<TransactionRecord, 'id' | 'timestamp'>) => string
updateTransaction: (id: string, updates: Partial<TransactionRecord>) => void
clearHistory: () => void
}
const TransactionContext = createContext<TransactionContextType | null>(null)
export function TransactionProvider({ children }: { children: ReactNode }) {
const history = useTransactionHistory()
return (
<TransactionContext.Provider value={history}>
{children}
</TransactionContext.Provider>
)
}
export function useTransactions() {
const context = useContext(TransactionContext)
if (!context) {
throw new Error('useTransactions must be used within TransactionProvider')
}
return context
}
export type { TransactionType, TransactionRecord }

View File

@@ -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<TransactionRecord[]>([])
// 从 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<TransactionRecord, 'id' | 'timestamp'>) => {
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<TransactionRecord>) => {
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,
}
}

View File

@@ -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",

View File

@@ -88,6 +88,22 @@
"en": "英文",
"zh": "中文"
},
"history": {
"title": "交易记录",
"empty": "暂无交易记录",
"clear": "清空",
"viewMore": "查看更多"
},
"toast": {
"txSubmitted": "交易已提交",
"txSuccess": "交易成功",
"txFailed": "交易失败",
"copySuccess": "已复制到剪贴板",
"walletError": "钱包错误",
"networkError": "网络错误",
"insufficientBalance": "余额不足",
"userRejected": "用户取消了交易"
},
"test": {
"title": "边界测试",
"currentStatus": "当前状态",

View File

@@ -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,
},
})