feat: 完成统计功能实现并修复 WebSocket 并发问题

- 实现全局统计接口(/api/copy-trading/statistics/global)
- 实现 Leader 统计接口(/api/copy-trading/statistics/leader)
- 实现分类统计接口(/api/copy-trading/statistics/category)
- 优化前端统计页面,支持时间范围筛选和响应式布局
- 修复 WebSocket 并发发送消息导致的状态冲突问题
  - 为每个会话添加同步锁,确保消息按顺序发送
  - 优化错误处理,避免 TEXT_PARTIAL_WRITING 异常
This commit is contained in:
WrBug
2025-12-03 02:02:18 +08:00
parent dec165cb24
commit 719b01508a
5 changed files with 397 additions and 14 deletions
+56 -5
View File
@@ -1,13 +1,20 @@
import { useEffect, useState } from 'react'
import { Card, Row, Col, Statistic, message } from 'antd'
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
import { Card, Row, Col, Statistic, message, DatePicker, Space, Button, Typography } from 'antd'
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined } from '@ant-design/icons'
import type { Dayjs } from 'dayjs'
import { apiService } from '../services/api'
import type { Statistics as StatisticsType } from '../types'
import { formatUSDC } from '../utils'
import { useMediaQuery } from 'react-responsive'
const { RangePicker } = DatePicker
const { Title } = Typography
const Statistics: React.FC = () => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const [stats, setStats] = useState<StatisticsType | null>(null)
const [loading, setLoading] = useState(false)
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null]>([null, null])
useEffect(() => {
fetchStatistics()
@@ -16,7 +23,10 @@ const Statistics: React.FC = () => {
const fetchStatistics = async () => {
setLoading(true)
try {
const response = await apiService.statistics.global()
const startTime = dateRange[0] ? dateRange[0].valueOf() : undefined
const endTime = dateRange[1] ? dateRange[1].valueOf() : undefined
const response = await apiService.statistics.global({ startTime, endTime })
if (response.data.code === 0 && response.data.data) {
setStats(response.data.data)
} else {
@@ -29,10 +39,49 @@ const Statistics: React.FC = () => {
}
}
const handleDateRangeChange = (dates: [Dayjs | null, Dayjs | null] | null) => {
setDateRange(dates || [null, null])
}
const handleReset = () => {
setDateRange([null, null])
// 重置后自动刷新
setTimeout(() => {
fetchStatistics()
}, 100)
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<h2></h2>
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px' }}>
<Title level={2} style={{ margin: 0 }}></Title>
<Space size="middle" wrap>
<RangePicker
value={dateRange}
onChange={handleDateRangeChange}
format="YYYY-MM-DD"
placeholder={['开始日期', '结束日期']}
size={isMobile ? 'middle' : 'large'}
allowClear
/>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={fetchStatistics}
loading={loading}
size={isMobile ? 'middle' : 'large'}
>
</Button>
{(dateRange[0] || dateRange[1]) && (
<Button
onClick={handleReset}
size={isMobile ? 'middle' : 'large'}
>
</Button>
)}
</Space>
</div>
<Row gutter={[16, 16]}>
@@ -73,6 +122,8 @@ const Statistics: React.FC = () => {
<Statistic
title="平均盈亏"
value={formatUSDC(stats?.avgPnl || '0')}
prefix={stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
suffix="USDC"
loading={loading}
/>