22// 中转站管理页:站点列表 + 添加/编辑弹窗 + 单站刷新/删除 + 余额趋势详情弹窗
33// 功能对照 v1 app.js:renderStations/stationRow(553-586、193-288)、站点表单弹窗(1487-1614)、
44// 趋势弹窗 openTrend/drawChart(1675-1822)——文案与数字口径逐条对齐,布局用 Pro 风格重排
5- import { useCallback , useEffect , useMemo , useState } from "react" ;
5+ import { useCallback , useEffect , useState } from "react" ;
66import { PageContainer , ProCard } from "@ant-design/pro-components" ;
77import {
88 App ,
@@ -25,12 +25,10 @@ import {
2525 DeleteOutlined ,
2626 CloseOutlined ,
2727} from "@ant-design/icons" ;
28- import { Line } from "@ant-design/plots" ;
2928import TrendModal from "../trend-modal" ;
3029import LastRefreshed from "../last-refreshed" ;
3130import dayjs from "dayjs" ;
3231import { api , cny , usd , rateOf , fmtTokens , fmtEta , statusOf } from "../../../lib/client" ;
33- import { useThemeMode } from "../../providers" ;
3432
3533// ---- 展示工具(v1 app.js 同名函数平移)--------------------------------------
3634const PLATE : Record < string , string > = { newapi : "NA" , "newapi-key" : "KEY" , sub2api : "S2" , "sub2api-password" : "S2" } ;
@@ -86,28 +84,31 @@ const hintStyle = (token: ReturnType<typeof theme.useToken>["token"]): React.CSS
8684 lineHeight : 1.6 ,
8785} ) ;
8886
89- // ---- 迷你余额走势(近 48 小时,v1 sparkSvg 的 plots 版)-----------------------
90- function Spark ( { pts } : { pts : any [ ] } ) {
91- const { dark } = useThemeMode ( ) ;
92- const data = useMemo ( ( ) => ( pts || [ ] ) . map ( ( p : any ) => ( { date : new Date ( p [ 0 ] ) , v : p [ 1 ] } ) ) , [ pts ] ) ;
87+ // ---- 迷你余额走势(近 48 小时,与总览卡片使用相同坐标口径)--------------------
88+ function Spark ( { pts } : { pts : [ number , number ] [ ] } ) {
89+ const { token } = theme . useToken ( ) ;
9390 if ( ! pts || pts . length < 2 ) return null ;
91+
92+ const W = 170 , H = 30 , P = 3 ;
93+ const t0 = pts [ 0 ] [ 0 ] , t1 = pts [ pts . length - 1 ] [ 0 ] ;
94+ let min = Infinity , max = - Infinity ;
95+ for ( const [ , v ] of pts ) {
96+ if ( v < min ) min = v ;
97+ if ( v > max ) max = v ;
98+ }
99+ if ( max - min < 1e-9 ) { min -= 1 ; max += 1 ; }
100+ const x = ( t : number ) => P + ( ( t - t0 ) / ( t1 - t0 || 1 ) ) * ( W - 2 * P ) ;
101+ const y = ( v : number ) => P + ( 1 - ( v - min ) / ( max - min ) ) * ( H - 2 * P ) ;
102+ const line = pts . map ( ( p , i ) => `${ i ? "L" : "M" } ${ x ( p [ 0 ] ) . toFixed ( 1 ) } ,${ y ( p [ 1 ] ) . toFixed ( 1 ) } ` ) . join ( "" ) ;
103+ const area = `${ line } L${ x ( t1 ) . toFixed ( 1 ) } ,${ H - P } L${ x ( t0 ) . toFixed ( 1 ) } ,${ H - P } Z` ;
104+ const last = pts [ pts . length - 1 ] ;
105+
94106 return (
95- < div style = { { width : 170 , maxWidth : "100%" , height : 30 , overflow : "hidden" } } >
96- < Line
97- data = { data }
98- xField = "date"
99- yField = "v"
100- width = { 170 }
101- height = { 30 }
102- axis = { false }
103- legend = { false }
104- tooltip = { false }
105- animate = { false }
106- padding = { 3 }
107- theme = { dark ? "classicDark" : "classic" }
108- style = { { lineWidth : 1.5 } }
109- />
110- </ div >
107+ < svg viewBox = { `0 0 ${ W } ${ H } ` } width = { 170 } height = { 30 } aria-hidden = "true" style = { { display : "block" , maxWidth : "100%" } } >
108+ < path d = { area } fill = { token . colorPrimaryBg } />
109+ < path d = { line } fill = "none" stroke = { token . colorPrimary } strokeWidth = { 1.5 } />
110+ < circle cx = { x ( last [ 0 ] ) . toFixed ( 1 ) } cy = { y ( last [ 1 ] ) . toFixed ( 1 ) } r = { 2.5 } fill = { token . colorPrimary } />
111+ </ svg >
111112 ) ;
112113}
113114
0 commit comments