@@ -125,6 +125,12 @@ interface PingTrendData {
125125 ping : number ;
126126}
127127
128+ // 添加连接池趋势数据类型 - 后端返回的是绝对值数据
129+ interface PoolTrendData {
130+ eventTime : string ;
131+ pool : number ;
132+ }
133+
128134// 添加流量单位转换函数
129135const formatTrafficValue = ( bytes : number ) => {
130136 const units = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' ] ;
@@ -211,6 +217,10 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
211217 const [ pingTrend , setPingTrend ] = React . useState < PingTrendData [ ] > ( [ ] ) ;
212218 const [ pingRefreshLoading , setPingRefreshLoading ] = React . useState ( false ) ;
213219 const [ pingTimeRange , setPingTimeRange ] = React . useState < "1h" | "6h" | "12h" | "24h" > ( "24h" ) ;
220+ // 连接池趋势相关状态
221+ const [ poolTrend , setPoolTrend ] = React . useState < PoolTrendData [ ] > ( [ ] ) ;
222+ const [ poolRefreshLoading , setPoolRefreshLoading ] = React . useState ( false ) ;
223+ const [ poolTimeRange , setPoolTimeRange ] = React . useState < "1h" | "6h" | "12h" | "24h" > ( "24h" ) ;
214224 const [ isPasswordVisible , setIsPasswordVisible ] = React . useState ( false ) ;
215225 // 编辑实例模态控制
216226 const [ editModalOpen , setEditModalOpen ] = React . useState ( false ) ;
@@ -309,6 +319,41 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
309319 return filteredData ;
310320 } , [ ] ) ;
311321
322+ // 根据时间范围过滤连接池数据
323+ const filterPoolDataByTimeRange = React . useCallback ( ( data : PoolTrendData [ ] , timeRange : "1h" | "6h" | "12h" | "24h" ) => {
324+ if ( data . length === 0 ) return data ;
325+
326+ // 获取当前时间
327+ const now = new Date ( ) ;
328+ const hoursAgo = timeRange === "1h" ? 1 : timeRange === "6h" ? 6 : timeRange === "12h" ? 12 : 24 ;
329+ const cutoffTime = new Date ( now . getTime ( ) - hoursAgo * 60 * 60 * 1000 ) ;
330+
331+ // 过滤数据
332+ const filteredData = data . filter ( ( item ) => {
333+ const timeStr = item . eventTime ;
334+ if ( ! timeStr ) return false ;
335+
336+ try {
337+ const [ datePart , timePart ] = timeStr . split ( ' ' ) ;
338+ if ( datePart && timePart ) {
339+ const [ year , month , day ] = datePart . split ( '-' ) . map ( Number ) ;
340+ const [ hour , minute ] = timePart . split ( ':' ) . map ( Number ) ;
341+ const itemTime = new Date ( year , month - 1 , day , hour , minute ) ;
342+ const isValid = ! isNaN ( itemTime . getTime ( ) ) ;
343+ const isInRange = isValid && itemTime >= cutoffTime ;
344+
345+ return isInRange ;
346+ }
347+ return false ;
348+ } catch ( error ) {
349+ console . error ( `连接池数据时间解析错误: ${ timeStr } ` , error ) ;
350+ return false ;
351+ }
352+ } ) ;
353+
354+ return filteredData ;
355+ } , [ ] ) ;
356+
312357 // 文件日志控制函数
313358 const handleLogRefresh = React . useCallback ( ( ) => {
314359 setLogRefreshTrigger ( prev => prev + 1 ) ;
@@ -407,6 +452,15 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
407452 setPingTrend ( pingData . pingTrend ) ;
408453 }
409454 }
455+
456+ // 获取连接池趋势数据
457+ const poolResponse = await fetch ( `/api/tunnels/${ resolvedId } /pool-trend` ) ;
458+ if ( poolResponse . ok ) {
459+ const poolData = await poolResponse . json ( ) ;
460+ if ( poolData . poolTrend && Array . isArray ( poolData . poolTrend ) ) {
461+ setPoolTrend ( poolData . poolTrend ) ;
462+ }
463+ }
410464
411465 // 刷新文件日志 - 直接更新trigger而不依赖handleLogRefresh
412466 setLogRefreshTrigger ( prev => prev + 1 ) ;
@@ -542,12 +596,48 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
542596 }
543597 } , [ resolvedId ] ) ;
544598
599+ // 获取连接池趋势数据
600+ const fetchPoolTrend = React . useCallback ( async ( ) => {
601+ try {
602+ setPoolRefreshLoading ( true ) ;
603+
604+ const response = await fetch ( `/api/tunnels/${ resolvedId } /pool-trend` ) ;
605+ if ( ! response . ok ) {
606+ throw new Error ( '获取连接池趋势失败' ) ;
607+ }
608+
609+ const data = await response . json ( ) ;
610+
611+ // 设置连接池趋势数据
612+ if ( data . poolTrend && Array . isArray ( data . poolTrend ) ) {
613+ setPoolTrend ( data . poolTrend ) ;
614+ console . log ( '[连接池趋势] 数据获取成功' , {
615+ 数据点数 : data . poolTrend . length ,
616+ 最新数据 : data . poolTrend [ data . poolTrend . length - 1 ] || null
617+ } ) ;
618+ } else {
619+ console . log ( '[连接池趋势] 数据为空或格式错误' , { poolTrend : data . poolTrend } ) ;
620+ setPoolTrend ( [ ] ) ;
621+ }
622+ } catch ( error ) {
623+ console . error ( '获取连接池趋势失败:' , error ) ;
624+ addToast ( {
625+ title : "获取连接池趋势失败" ,
626+ description : error instanceof Error ? error . message : "未知错误" ,
627+ color : "danger" ,
628+ } ) ;
629+ } finally {
630+ setPoolRefreshLoading ( false ) ;
631+ }
632+ } , [ resolvedId ] ) ;
633+
545634 // 初始加载数据
546635 React . useEffect ( ( ) => {
547636 fetchTunnelDetails ( ) ;
548637 fetchTrafficTrend ( ) ;
549638 fetchPingTrend ( ) ;
550- } , [ fetchTunnelDetails , fetchTrafficTrend , fetchPingTrend ] ) ;
639+ fetchPoolTrend ( ) ;
640+ } , [ fetchTunnelDetails , fetchTrafficTrend , fetchPingTrend , fetchPoolTrend ] ) ;
551641
552642 // SSE监听逻辑
553643 useTunnelSSE ( tunnelInfo ?. instanceId || "" , {
@@ -574,6 +664,8 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
574664 fetchTrafficTrend ( ) ;
575665 // 刷新延迟趋势
576666 fetchPingTrend ( ) ;
667+ // 刷新连接池趋势
668+ fetchPoolTrend ( ) ;
577669 // 刷新日志(通过触发器)
578670 setLogRefreshTrigger ( prev => prev + 1 ) ;
579671
@@ -1072,7 +1164,14 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
10721164 < CellValue label = "实例ID" value = { tunnelInfo . instanceId } />
10731165 < CellValue
10741166 label = "主控"
1075- value = { < Chip variant = "bordered" color = "default" size = "sm" > { tunnelInfo . endpoint } </ Chip > }
1167+ value = {
1168+ < div className = "flex items-center gap-2" >
1169+ < Chip variant = "bordered" color = "default" size = "sm" > { tunnelInfo . endpoint } </ Chip >
1170+ < Chip variant = "flat" color = "secondary" size = "sm" >
1171+ { tunnelInfo . endpointVersion || "< v1.4.0" }
1172+ </ Chip >
1173+ </ div >
1174+ }
10761175 />
10771176
10781177 < CellValue
@@ -1214,7 +1313,7 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
12141313 key = "command"
12151314 aria-label = "命令行"
12161315 title = {
1217- < h3 className = "text-lg font-semibold" > 命令行</ h3 >
1316+ < h3 className = "text-lg font-semibold ps-1 " > 命令行</ h3 >
12181317 }
12191318 >
12201319 < div className = "pb-4" >
@@ -1408,93 +1507,127 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
14081507 </ div >
14091508 </ CardBody >
14101509 </ Card >
1411-
1412- { /* 延迟趋势图表 - 仅服务端隧道显示 */ }
1413- { tunnelInfo . type === '服务端1' && (
1414- < Card className = "p-2" >
1415- < CardHeader className = "font-bold text-sm md:text-base justify-between" >
1416- < div className = "flex items-center gap-3" >
1417- < div className = "flex items-center gap-2" >
1418- 延迟趋势
1419- < Tooltip content = "仅服务端隧道显示延迟数据" placement = "top" >
1420- < FontAwesomeIcon
1421- icon = { faQuestionCircle }
1422- className = "text-default-400 hover:text-default-600 cursor-help text-xs"
1423- />
1424- </ Tooltip >
1425- </ div >
1426- </ div >
1510+
1511+ { /* 连接池变化 - 独立Card - 只有当traffic.pool不为null时才显示 */ }
1512+ { tunnelInfo . traffic . pool !== null && (
1513+ < Card className = "p-2" >
1514+ < CardHeader className = "flex items-center justify-between" >
1515+ < div className = "flex items-center gap-3" >
14271516 < div className = "flex items-center gap-2" >
1428- { /* 刷新按钮 */ }
1429- < Button
1430- size = "sm"
1431- variant = "flat"
1432- isIconOnly
1433- onPress = { fetchTrafficTrend }
1434- isLoading = { trafficRefreshLoading }
1435- className = "h-7 w-7 min-w-0"
1436- >
1437- < FontAwesomeIcon icon = { faRefresh } className = "text-xs" />
1438- </ Button >
1439- { /* 时间范围选择 */ }
1440- < Tabs
1441- selectedKey = { trafficTimeRange }
1442- onSelectionChange = { ( key ) => setTrafficTimeRange ( key as "1h" | "6h" | "12h" | "24h" ) }
1443- size = "sm"
1444- variant = "light"
1445- classNames = { {
1446- tabList : "gap-1" ,
1447- tab : "text-xs px-2 py-1 min-w-0 h-7" ,
1448- tabContent : "text-xs"
1449- } }
1450- >
1451- < Tab key = "1h" title = "1小时" />
1452- < Tab key = "6h" title = "6小时" />
1453- < Tab key = "12h" title = "12小时" />
1454- < Tab key = "24h" title = "24小时" />
1455- </ Tabs >
1517+ < h3 className = "text-lg font-semibold" > 连接池变化</ h3 >
1518+ < Tooltip content = "显示隧道连接池数量的变化趋势" placement = "right" >
1519+ < FontAwesomeIcon
1520+ icon = { faQuestionCircle }
1521+ className = "text-default-400 hover:text-default-600 cursor-help text-xs"
1522+ />
1523+ </ Tooltip >
14561524 </ div >
1457- </ CardHeader >
1458- < CardBody >
1459- < div className = "h-64" >
1460- { ( ( ) => {
1461- // 使用过滤后的数据计算单位
1462- if ( ! trafficTrend || ! Array . isArray ( trafficTrend ) || trafficTrend . length === 0 ) {
1463- return < div className = "flex items-center justify-center h-full text-default-400" > 暂无延迟数据</ div > ;
1464- }
1465-
1466- const filteredData = filterDataByTimeRange ( trafficTrend , trafficTimeRange ) ;
1467- if ( filteredData . length === 0 ) {
1468- return < div className = "flex items-center justify-center h-full text-default-400" > 所选时间范围内暂无延迟数据</ div > ;
1469- }
1470-
1471- // 检查是否有延迟数据
1472- const hasPingData = filteredData . some ( ( item : TrafficTrendData ) => item . pingDiff !== null && item . pingDiff !== undefined ) ;
1473- if ( ! hasPingData ) {
1474- return < div className = "flex items-center justify-center h-full text-default-400" > 暂无延迟数据</ div > ;
1475- }
1476-
1477- return (
1478- < FlowTrafficChart
1479- data = { [ {
1480- id : `延迟` ,
1481- data : filteredData . map ( ( item : TrafficTrendData ) => ( {
1482- x : item . eventTime || '' ,
1483- y : Number ( item . pingDiff ) || 0 ,
1484- unit : 'ms'
1525+ </ div >
1526+ < div className = "flex items-center gap-2" >
1527+ { /* 刷新按钮 */ }
1528+ < Button
1529+ size = "sm"
1530+ variant = "flat"
1531+ isIconOnly
1532+ onPress = { fetchPoolTrend }
1533+ isLoading = { poolRefreshLoading }
1534+ className = "h-7 w-7 min-w-0"
1535+ >
1536+ < FontAwesomeIcon icon = { faRefresh } className = "text-xs" />
1537+ </ Button >
1538+ { /* 时间范围选择 */ }
1539+ < Tabs
1540+ selectedKey = { poolTimeRange }
1541+ onSelectionChange = { ( key ) => setPoolTimeRange ( key as "1h" | "6h" | "12h" | "24h" ) }
1542+ size = "sm"
1543+ variant = "light"
1544+ classNames = { {
1545+ tabList : "gap-1" ,
1546+ tab : "text-xs px-2 py-1 min-w-0 h-7" ,
1547+ tabContent : "text-xs"
1548+ } }
1549+ >
1550+ < Tab key = "1h" title = "1小时" />
1551+ < Tab key = "6h" title = "6小时" />
1552+ < Tab key = "12h" title = "12小时" />
1553+ < Tab key = "24h" title = "24小时" />
1554+ </ Tabs >
1555+ </ div >
1556+ </ CardHeader >
1557+ < CardBody >
1558+ < div className = "h-[250px] md:h-[300px]" >
1559+ { loading ? (
1560+ < div className = "flex items-center justify-center h-full" >
1561+ < div className = "space-y-4 text-center" >
1562+ < div className = "flex justify-center" >
1563+ < div className = "relative w-8 h-8" >
1564+ < div className = "absolute inset-0 rounded-full border-4 border-default-200 border-t-primary animate-spin" />
1565+ </ div >
1566+ </ div >
1567+ < p className = "text-default-500 animate-pulse text-sm md:text-base" > 加载连接池数据中...</ p >
1568+ </ div >
1569+ </ div >
1570+ ) : ( ( ) => {
1571+ // 检查原始数据是否为空
1572+ if ( ! poolTrend || ! Array . isArray ( poolTrend ) || poolTrend . length === 0 ) {
1573+ return true ; // 显示占位符
1574+ }
1575+
1576+ // 检查过滤后的数据是否为空
1577+ const filteredData = filterPoolDataByTimeRange ( poolTrend , poolTimeRange ) ;
1578+ return filteredData . length === 0 ;
1579+ } ) ( ) ? (
1580+ < div className = "flex items-center justify-center h-full" >
1581+ < div className = "text-center" >
1582+ < p className = "text-default-500 text-base md:text-lg" > 暂无连接池数据</ p >
1583+ < p className = "text-default-400 text-xs md:text-sm mt-2" >
1584+ { ! poolTrend || poolTrend . length === 0
1585+ ? "当实例运行时,连接池变化数据将在此显示"
1586+ : `在过去${ poolTimeRange === "1h" ? "1小时" : poolTimeRange === "6h" ? "6小时" : poolTimeRange === "12h" ? "12小时" : "24小时" } 内暂无连接池数据`
1587+ }
1588+ </ p >
1589+ </ div >
1590+ </ div >
1591+ ) : (
1592+ < FlowTrafficChart
1593+ key = { `pool-${ poolTimeRange } -${ poolTrend ?. length || 0 } ` } // 强制重新渲染
1594+ timeRange = { poolTimeRange }
1595+ showLegend = { false }
1596+ data = { ( ( ) => {
1597+ // 安全检查
1598+ if ( ! poolTrend || ! Array . isArray ( poolTrend ) || poolTrend . length === 0 ) {
1599+ return [ ] ;
1600+ }
1601+
1602+ // 首先根据时间范围过滤数据
1603+ const filteredData = filterPoolDataByTimeRange ( poolTrend , poolTimeRange ) ;
1604+
1605+ if ( filteredData . length === 0 ) return [ ] ;
1606+
1607+ const chartData = [
1608+ {
1609+ id : `连接池` ,
1610+ data : filteredData . map ( ( item : PoolTrendData ) => ( {
1611+ x : item . eventTime || '' , // 直接使用后端返回的格式 "2025-06-26 18:40"
1612+ y : Number ( item . pool ) || 0 ,
1613+ unit : '个'
14851614 } ) )
1486- } ] }
1487- unit = "ms"
1488- />
1489- ) ;
1490- } ) ( ) }
1491- </ div >
1492- </ CardBody >
1493- </ Card >
1494- ) }
1615+ }
1616+ ] ;
1617+
1618+ return chartData ;
1619+ } ) ( ) }
1620+ unit = "个"
1621+ />
1622+ ) }
1623+ </ div >
1624+ </ CardBody >
1625+ </ Card >
1626+ ) }
14951627
1496- { /* 延迟变化 - 独立Card */ }
1497- < Card className = "p-2" >
1628+ { /* 延迟变化 - 独立Card - 只有当traffic.ping不为null时才显示 */ }
1629+ { tunnelInfo . traffic . ping !== null && (
1630+ < Card className = "p-2" >
14981631 < CardHeader className = "flex items-center justify-between" >
14991632 < div className = "flex items-center gap-3" >
15001633 < div className = "flex items-center gap-2" >
@@ -1607,6 +1740,8 @@ export default function TunnelDetailPage({ params }: { params: Promise<PageParam
16071740 </ div >
16081741 </ CardBody >
16091742 </ Card >
1743+ ) }
1744+
16101745
16111746 { /* 日志 - 独立Card */ }
16121747 < Card className = "p-2" >
0 commit comments