diff --git a/.cursor/rules/frontend.mdc b/.cursor/rules/frontend.mdc index d1fed7a..8929b54 100644 --- a/.cursor/rules/frontend.mdc +++ b/.cursor/rules/frontend.mdc @@ -176,6 +176,80 @@ export const useMarketStore = create((set) => ({ })); ``` +### USDC 金额格式化规范 +- **必须**使用 `formatUSDC` 函数格式化所有 USDC 金额显示 +- **禁止**直接使用 `toFixed()` 或 `parseFloat().toFixed()` 格式化 USDC +- **禁止**硬编码小数位数格式化 USDC +- 所有 USDC 金额显示必须统一使用 `formatUSDC` 函数 + +#### formatUSDC 函数说明 +- **位置**: `src/utils/index.ts` +- **功能**: 格式化 USDC 金额,最多显示 4 位小数,自动去除尾随零(截断,不四舍五入) +- **参数**: `value: string | number | undefined | null` +- **返回值**: 格式化后的字符串,如果值为空或无效则返回 `'-'` + +#### 使用示例 + +```typescript +// ✅ 正确:使用 formatUSDC 格式化 USDC 金额 +import { formatUSDC } from '../utils' + +const BalanceDisplay: React.FC<{ balance: string }> = ({ balance }) => { + return {formatUSDC(balance)} USDC +} + +// ✅ 正确:在表格列中使用 +const columns = [ + { + title: '余额', + dataIndex: 'balance', + render: (balance: string) => `${formatUSDC(balance)} USDC` + } +] + +// ✅ 正确:在统计组件中使用 + + +// ❌ 错误:直接使用 toFixed +const balance = parseFloat(value).toFixed(4) // 禁止 + +// ❌ 错误:硬编码格式化 +const balance = `${parseFloat(value).toFixed(2)} USDC` // 禁止 + +// ❌ 错误:使用自定义格式化函数 +const formatBalance = (value: string) => parseFloat(value).toFixed(4) // 禁止 +``` + +#### 格式化规则 +- **最多显示 4 位小数**:如果金额超过 4 位小数,截断到 4 位(不四舍五入) +- **自动去除尾随零**:去除不必要的尾随零和小数点 +- **示例**: + - `formatUSDC(1.23)` => `"1.23"` + - `formatUSDC(1.23456)` => `"1.2345"`(截断,不四舍五入) + - `formatUSDC(1.2)` => `"1.2"` + - `formatUSDC(1)` => `"1"` + - `formatUSDC(1.23459)` => `"1.2345"`(截断,不四舍五入) + - `formatUSDC(null)` => `"-"` + - `formatUSDC(undefined)` => `"-"` + +#### 工具函数统一导出 +- 所有工具函数统一从 `src/utils/index.ts` 导出 +- **必须**从 `../utils` 导入工具函数,而不是从具体文件导入 +- `ethers.ts` 中的函数也会在 `index.ts` 中统一导出 + +```typescript +// ✅ 正确:从 utils 统一导入 +import { formatUSDC, getAddressFromPrivateKey, isValidWalletAddress } from '../utils' + +// ❌ 错误:从具体文件导入 +import { formatUSDC } from '../utils/index' +import { getAddressFromPrivateKey } from '../utils/ethers' +``` + ## 移动端适配示例 ### 响应式布局 diff --git a/frontend/src/pages/AccountDetail.tsx b/frontend/src/pages/AccountDetail.tsx index 97a6069..9e90877 100644 --- a/frontend/src/pages/AccountDetail.tsx +++ b/frontend/src/pages/AccountDetail.tsx @@ -5,6 +5,7 @@ import { ArrowLeftOutlined, ReloadOutlined, EditOutlined } from '@ant-design/ico import { useAccountStore } from '../store/accountStore' import type { Account } from '../types' import { useMediaQuery } from 'react-responsive' +import { formatUSDC } from '../utils' const { Title } = Typography @@ -212,7 +213,7 @@ const AccountDetail: React.FC = () => { ) : balance ? ( - {balance} USDC + {formatUSDC(balance)} USDC ) : ( - @@ -307,7 +308,7 @@ const AccountDetail: React.FC = () => { fontWeight: 'bold', color: account.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a' }}> - {account.totalPnl} USDC + {formatUSDC(account.totalPnl)} USDC )} diff --git a/frontend/src/pages/AccountImport.tsx b/frontend/src/pages/AccountImport.tsx index d092946..8479c10 100644 --- a/frontend/src/pages/AccountImport.tsx +++ b/frontend/src/pages/AccountImport.tsx @@ -10,7 +10,7 @@ import { isValidWalletAddress, isValidPrivateKey, isValidMnemonic -} from '../utils/ethers' +} from '../utils' import { useMediaQuery } from 'react-responsive' const { Title } = Typography diff --git a/frontend/src/pages/AccountList.tsx b/frontend/src/pages/AccountList.tsx index 6fd626d..ec08f8a 100644 --- a/frontend/src/pages/AccountList.tsx +++ b/frontend/src/pages/AccountList.tsx @@ -5,6 +5,7 @@ import { PlusOutlined, StarOutlined, StarFilled, ReloadOutlined, EditOutlined } import { useAccountStore } from '../store/accountStore' import type { Account } from '../types' import { useMediaQuery } from 'react-responsive' +import { formatUSDC } from '../utils' const { Title } = Typography @@ -261,7 +262,7 @@ const AccountList: React.FC = () => { } const balanceObj = balanceMap[record.id] const balance = balanceObj?.total || record.balance || '-' - return balance && balance !== '-' && typeof balance === 'string' ? `${balance} USDC` : '-' + return balance && balance !== '-' && typeof balance === 'string' ? `${formatUSDC(balance)} USDC` : '-' } }, { @@ -359,7 +360,7 @@ const AccountList: React.FC = () => { 总余额: {balanceLoading[record.id] ? ( ) : balanceMap[record.id]?.total && balanceMap[record.id].total !== '-' ? ( - `${balanceMap[record.id].total} USDC` + `${formatUSDC(balanceMap[record.id].total)} USDC` ) : ( '-' )} @@ -370,7 +371,7 @@ const AccountList: React.FC = () => { color: '#666', marginTop: '4px' }}> - 可用: {balanceMap[record.id].available} USDC | 仓位: {balanceMap[record.id].position} USDC + 可用: {formatUSDC(balanceMap[record.id].available)} USDC | 仓位: {formatUSDC(balanceMap[record.id].position)} USDC )} {(record.activeOrders !== undefined && record.activeOrders !== null) && ( @@ -597,7 +598,7 @@ const AccountList: React.FC = () => { ) : detailBalance ? ( - {detailBalance.total} USDC + {formatUSDC(detailBalance.total)} USDC ) : ( - @@ -608,7 +609,7 @@ const AccountList: React.FC = () => { ) : detailBalance ? ( - {detailBalance.available} USDC + {formatUSDC(detailBalance.available)} USDC ) : ( - @@ -619,7 +620,7 @@ const AccountList: React.FC = () => { ) : detailBalance ? ( - {detailBalance.position} USDC + {formatUSDC(detailBalance.position)} USDC ) : ( - @@ -696,7 +697,7 @@ const AccountList: React.FC = () => { fontWeight: 'bold', color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a' }}> - {detailAccount.totalPnl} USDC + {formatUSDC(detailAccount.totalPnl)} USDC )} diff --git a/frontend/src/pages/CopyTradingAdd.tsx b/frontend/src/pages/CopyTradingAdd.tsx index c0354a3..843d058 100644 --- a/frontend/src/pages/CopyTradingAdd.tsx +++ b/frontend/src/pages/CopyTradingAdd.tsx @@ -6,6 +6,7 @@ import { apiService } from '../services/api' import { useAccountStore } from '../store/accountStore' import type { Account, Leader, CopyTradingTemplate } from '../types' import { useMediaQuery } from 'react-responsive' +import { formatUSDC } from '../utils' const { Title } = Typography const { Option } = Select @@ -114,7 +115,7 @@ const CopyTradingAdd: React.FC = () => { diff --git a/frontend/src/pages/OrderList.tsx b/frontend/src/pages/OrderList.tsx index 87f3fcc..16dded6 100644 --- a/frontend/src/pages/OrderList.tsx +++ b/frontend/src/pages/OrderList.tsx @@ -3,6 +3,7 @@ import { Card, Table, Tag, message } from 'antd' import { apiService } from '../services/api' import type { CopyOrder } from '../types' import { useMediaQuery } from 'react-responsive' +import { formatUSDC } from '../utils' const OrderList: React.FC = () => { const isMobile = useMediaQuery({ maxWidth: 768 }) @@ -115,7 +116,7 @@ const OrderList: React.FC = () => { key: 'pnl', render: (pnl: string | undefined) => pnl ? ( - {pnl} USDC + {formatUSDC(pnl)} USDC ) : '-' }, diff --git a/frontend/src/pages/PositionList.tsx b/frontend/src/pages/PositionList.tsx index c78e125..8a2e6fd 100644 --- a/frontend/src/pages/PositionList.tsx +++ b/frontend/src/pages/PositionList.tsx @@ -7,6 +7,7 @@ import { getPositionKey } from '../types' import { useMediaQuery } from 'react-responsive' import { useWebSocketSubscription } from '../hooks/useWebSocket' import { wsManager } from '../services/websocket' +import { formatUSDC } from '../utils' type PositionFilter = 'current' | 'historical' type ViewMode = 'card' | 'list' @@ -611,7 +612,7 @@ const PositionList: React.FC = () => { fontWeight: '500', color: isProfit ? '#52c41a' : '#f5222d' }}> - {pnlNum >= 0 ? '+' : ''}{formatNumber(position.pnl, 2)} USDC + {pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC )} @@ -634,7 +635,7 @@ const PositionList: React.FC = () => {
开仓价值 - {formatNumber(position.initialValue, 2)} USDC + {formatUSDC(position.initialValue)} USDC
{positionFilter === 'current' && position.currentPrice && ( @@ -648,7 +649,7 @@ const PositionList: React.FC = () => {
当前价值 - {formatNumber(position.currentValue, 2)} USDC + {formatUSDC(position.currentValue)} USDC
@@ -691,7 +692,7 @@ const PositionList: React.FC = () => { fontWeight: 'bold', color: isProfit ? '#52c41a' : '#f5222d' }}> - {pnlNum >= 0 ? '+' : ''}{formatNumber(position.pnl, 2)} USDC + {pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
@@ -718,7 +719,7 @@ const PositionList: React.FC = () => { color: parseFloat(position.realizedPnl) >= 0 ? '#52c41a' : '#f5222d', fontWeight: '500' }}> - {parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatNumber(position.realizedPnl, 2)} USDC + {parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatUSDC(position.realizedPnl)} USDC
)} @@ -872,7 +873,7 @@ const PositionList: React.FC = () => { key: 'initialValue', render: (value: string) => ( - {formatNumber(value, 2)} USDC + {formatUSDC(value)} USDC ), align: 'right' as const, @@ -897,7 +898,7 @@ const PositionList: React.FC = () => { key: 'currentValue', render: (value: string) => ( - {formatNumber(value, 2)} USDC + {formatUSDC(value)} USDC ), align: 'right' as const, @@ -928,7 +929,7 @@ const PositionList: React.FC = () => { color: pnlNum >= 0 ? '#3f8600' : '#cf1322', fontWeight: 'bold' }}> - {pnlNum >= 0 ? '+' : ''}{formatNumber(pnl, 2)} USDC + {pnlNum >= 0 ? '+' : ''}{formatUSDC(pnl)} USDC
{ color: pnlNum >= 0 ? '#3f8600' : '#cf1322', fontWeight: 'bold' }}> - {pnlNum >= 0 ? '+' : ''}{formatNumber(realizedPnl, 2)} USDC + {pnlNum >= 0 ? '+' : ''}{formatUSDC(realizedPnl)} USDC
{record.percentRealizedPnl && (
{ borderColor: '#52c41a' }} > - 赎回 ({redeemableSummary.totalCount}个, {formatNumber(redeemableSummary.totalValue, 2)} USDC) + 赎回 ({redeemableSummary.totalCount}个, {formatUSDC(redeemableSummary.totalValue)} USDC) )}
@@ -1214,14 +1215,14 @@ const PositionList: React.FC = () => { 开仓价值合计:{' '} - {formatNumber(positionTotals.totalInitialValue.toString(), 2)} USDC + {formatUSDC(positionTotals.totalInitialValue.toString())} USDC 当前价值合计:{' '} {positionFilter === 'current' - ? `${formatNumber(positionTotals.totalCurrentValue.toString(), 2)} USDC` + ? `${formatUSDC(positionTotals.totalCurrentValue.toString())} USDC` : '-'} @@ -1234,7 +1235,7 @@ const PositionList: React.FC = () => { }} > {positionTotals.totalPnl >= 0 ? '+' : ''} - {formatNumber(positionTotals.totalPnl.toString(), 2)} USDC + {formatUSDC(positionTotals.totalPnl.toString())} USDC @@ -1246,7 +1247,7 @@ const PositionList: React.FC = () => { }} > {positionTotals.totalRealizedPnl >= 0 ? '+' : ''} - {formatNumber(positionTotals.totalRealizedPnl.toString(), 2)} USDC + {formatUSDC(positionTotals.totalRealizedPnl.toString())} USDC @@ -1460,7 +1461,7 @@ const PositionList: React.FC = () => { color: currentPnl.pnl >= 0 ? '#52c41a' : '#f5222d', marginBottom: '4px' }}> - {currentPnl.pnl >= 0 ? '+' : ''}{currentPnl.pnl.toFixed(2)} USDC + {currentPnl.pnl >= 0 ? '+' : ''}{formatUSDC(currentPnl.pnl)} USDC
{ - {formatNumber(redeemableSummary.totalValue, 2)} USDC + {formatUSDC(redeemableSummary.totalValue)} USDC diff --git a/frontend/src/pages/Statistics.tsx b/frontend/src/pages/Statistics.tsx index 321fe08..9839b68 100644 --- a/frontend/src/pages/Statistics.tsx +++ b/frontend/src/pages/Statistics.tsx @@ -3,6 +3,7 @@ import { Card, Row, Col, Statistic, message } from 'antd' import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import type { Statistics as StatisticsType } from '../types' +import { formatUSDC } from '../utils' const Statistics: React.FC = () => { const [stats, setStats] = useState(null) @@ -48,8 +49,7 @@ const Statistics: React.FC = () => { = 0 ? : } valueStyle={{ color: stats?.totalPnl && parseFloat(stats.totalPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }} suffix="USDC" @@ -72,8 +72,7 @@ const Statistics: React.FC = () => { @@ -83,8 +82,7 @@ const Statistics: React.FC = () => { } valueStyle={{ color: '#3f8600' }} suffix="USDC" @@ -96,8 +94,7 @@ const Statistics: React.FC = () => { } valueStyle={{ color: '#cf1322' }} suffix="USDC" diff --git a/frontend/src/pages/TemplateList.tsx b/frontend/src/pages/TemplateList.tsx index 11df1b9..a85e241 100644 --- a/frontend/src/pages/TemplateList.tsx +++ b/frontend/src/pages/TemplateList.tsx @@ -5,6 +5,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined } from '@ant-d import { apiService } from '../services/api' import type { CopyTradingTemplate } from '../types' import { useMediaQuery } from 'react-responsive' +import { formatUSDC } from '../utils' const { Search } = Input @@ -163,7 +164,7 @@ const TemplateList: React.FC = () => { if (record.copyMode === 'RATIO') { return `比例 ${record.copyRatio}x` } else if (record.copyMode === 'FIXED' && record.fixedAmount) { - return `固定 ${parseFloat(record.fixedAmount).toFixed(4)} USDC` + return `固定 ${formatUSDC(record.fixedAmount)} USDC` } return '-' } @@ -331,7 +332,7 @@ const TemplateList: React.FC = () => { {template.copyMode === 'RATIO' ? `比例 ${template.copyRatio}x` : template.fixedAmount - ? `固定 ${parseFloat(template.fixedAmount).toFixed(4)} USDC` + ? `固定 ${formatUSDC(template.fixedAmount)} USDC` : '-' }
@@ -343,11 +344,11 @@ const TemplateList: React.FC = () => {
金额限制
{template.maxOrderSize && ( - 最大: {parseFloat(template.maxOrderSize).toFixed(4)} USDC + 最大: {formatUSDC(template.maxOrderSize)} USDC )} {template.maxOrderSize && template.minOrderSize && | } {template.minOrderSize && ( - 最小: {parseFloat(template.minOrderSize).toFixed(4)} USDC + 最小: {formatUSDC(template.minOrderSize)} USDC )} {!template.maxOrderSize && !template.minOrderSize && 未设置}
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..f2bd701 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,39 @@ +/** + * 格式化 USDC 金额 + * 最多显示 4 位小数,自动去除尾随零(截断,不四舍五入) + * @param value - 金额值(字符串或数字) + * @returns 格式化后的字符串,如果值为空或无效则返回 '-' + * @example + * formatUSDC(1.23) => "1.23" + * formatUSDC(1.23456) => "1.2345" + * formatUSDC(1.2) => "1.2" + * formatUSDC(1) => "1" + */ +export const formatUSDC = (value: string | number | undefined | null): string => { + if (value === undefined || value === null || value === '') { + return '-' + } + + const num = typeof value === 'string' ? parseFloat(value) : value + if (isNaN(num)) { + return '-' + } + + // 使用 Math.floor 截断到4位小数(不四舍五入) + const multiplier = Math.pow(10, 4) + const truncated = Math.floor(num * multiplier) / multiplier + + // 使用 toFixed(4) 确保格式一致,然后去除尾随零和小数点 + return truncated.toFixed(4).replace(/\.?0+$/, '') +} + +// 统一导出 ethers 相关工具函数 +export { + getAddressFromPrivateKey, + getAddressFromMnemonic, + getPrivateKeyFromMnemonic, + isValidMnemonic, + isValidWalletAddress, + isValidPrivateKey +} from './ethers' +