refactor(dashboard): simplify components and fix efficiency issues

- Extract DecisionBadge and StatusIcon/StatusTag to shared components
  to eliminate duplication across BatchManager, AnalysisMonitor, PortfolioPanel
- Remove dead code: unused maxConcurrent state and formatTime function
- Add useMemo for columns (all pages) and derived stats (BatchManager, PortfolioPanel)
- Fix polling flash: BatchManager fetchTasks accepts showLoading param
- Fix RecommendationsTab: consolidate progress completion into connectWs handler,
  replace double-arrow cleanup with named cleanup function
- Extract DEFAULT_ACCOUNT constant to avoid magic strings
- Extract HEADER_LABEL_STYLE and HEADER_ICON_STYLE constants in ScreeningPanel
- Remove unused imports (CheckCircleOutlined, CloseCircleOutlined, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
陈少杰 2026-04-07 20:27:49 +08:00
parent f196962112
commit 04ac20ca69
7 changed files with 145 additions and 142 deletions

View File

@ -0,0 +1,5 @@
export default function DecisionBadge({ decision }) {
if (!decision) return null
const cls = decision === 'BUY' ? 'badge-buy' : decision === 'SELL' ? 'badge-sell' : 'badge-hold'
return <span className={cls}>{decision}</span>
}

View File

@ -0,0 +1,49 @@
import { CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
const STATUS_TAG_MAP = {
pending: { text: '等待', bg: 'var(--bg-elevated)', color: 'var(--text-muted)' },
running: { text: '分析中', bg: 'var(--running-dim)', color: 'var(--running)' },
completed: { text: '完成', bg: 'var(--buy-dim)', color: 'var(--buy)' },
failed: { text: '失败', bg: 'var(--sell-dim)', color: 'var(--sell)' },
}
export function StatusIcon({ status }) {
switch (status) {
case 'completed':
return <CheckCircleOutlined style={{ color: 'var(--buy)', fontSize: 16 }} />
case 'failed':
return <CloseCircleOutlined style={{ color: 'var(--sell)', fontSize: 16 }} />
case 'running':
return <SyncOutlined spin style={{ color: 'var(--running)', fontSize: 16 }} />
default:
return (
<span
style={{
width: 16,
height: 16,
borderRadius: '50%',
border: '2px solid var(--border-strong)',
display: 'inline-block',
}}
/>
)
}
}
export function StatusTag({ status }) {
const s = STATUS_TAG_MAP[status] || STATUS_TAG_MAP.pending
return (
<span
style={{
background: s.bg,
color: s.color,
padding: '2px 10px',
borderRadius: 'var(--radius-pill)',
fontSize: 12,
fontWeight: 600,
}}
>
{s.text}
</span>
)
}

View File

@ -1,7 +1,8 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { Card, Progress, Badge, Empty, Button, Result, message } from 'antd' import { Card, Progress, Badge, Empty, Button, Result, message } from 'antd'
import { CheckCircleOutlined, SyncOutlined, CloseCircleOutlined } from '@ant-design/icons' import DecisionBadge from '../components/DecisionBadge'
import { StatusIcon } from '../components/StatusIcon'
const ANALYSIS_STAGES = [ const ANALYSIS_STAGES = [
{ key: 'analysts', label: '分析师团队' }, { key: 'analysts', label: '分析师团队' },
@ -79,31 +80,6 @@ export default function AnalysisMonitor() {
} }
}, [taskId, fetchInitialState, connectWebSocket]) }, [taskId, fetchInitialState, connectWebSocket])
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const getStageIcon = (status) => {
switch (status) {
case 'completed':
return <CheckCircleOutlined style={{ color: 'var(--color-buy)', fontSize: 16 }} />
case 'running':
return <SyncOutlined spin style={{ color: 'var(--color-running)', fontSize: 16 }} />
case 'failed':
return <CloseCircleOutlined style={{ color: 'var(--color-sell)', fontSize: 16 }} />
default:
return <span style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid var(--border-strong)', display: 'inline-block' }} />
}
}
const getDecisionBadge = (decision) => {
if (!decision) return null
const badgeClass = decision === 'BUY' ? 'badge-buy' : decision === 'SELL' ? 'badge-sell' : 'badge-hold'
return <span className={badgeClass}>{decision}</span>
}
if (!taskId) { if (!taskId) {
return ( return (
<div className="card"> <div className="card">
@ -179,7 +155,7 @@ export default function AnalysisMonitor() {
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 28, fontWeight: 600, letterSpacing: 0.196, lineHeight: 1.14 }}> <span style={{ fontFamily: 'var(--font-ui)', fontSize: 28, fontWeight: 600, letterSpacing: 0.196, lineHeight: 1.14 }}>
{task.ticker} {task.ticker}
</span> </span>
{getDecisionBadge(task.decision)} <DecisionBadge decision={task.decision} />
</div> </div>
{/* Progress */} {/* Progress */}
@ -200,7 +176,7 @@ export default function AnalysisMonitor() {
const status = stageState?.status || 'pending' const status = stageState?.status || 'pending'
return ( return (
<div key={stage.key} className={`stage-pill ${status}`}> <div key={stage.key} className={`stage-pill ${status}`}>
{getStageIcon(status)} <StatusIcon status={status} />
<span>{stage.label}</span> <span>{stage.label}</span>
</div> </div>
) )

View File

@ -1,17 +1,16 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback, useMemo } from 'react'
import { Table, Button, Progress, Result, Empty, Card, message, Popconfirm, Tooltip } from 'antd' import { Table, Button, Progress, Result, Card, message, Popconfirm, Tooltip } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, SyncOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons' import { DeleteOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
import DecisionBadge from '../components/DecisionBadge'
const MAX_CONCURRENT = 3 import { StatusIcon, StatusTag } from '../components/StatusIcon'
export default function BatchManager() { export default function BatchManager() {
const [tasks, setTasks] = useState([]) const [tasks, setTasks] = useState([])
const [maxConcurrent] = useState(MAX_CONCURRENT)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const fetchTasks = useCallback(async () => { const fetchTasks = useCallback(async (showLoading = true) => {
setLoading(true) if (showLoading) setLoading(true)
try { try {
const res = await fetch('/api/analysis/tasks') const res = await fetch('/api/analysis/tasks')
if (!res.ok) throw new Error('获取任务列表失败') if (!res.ok) throw new Error('获取任务列表失败')
@ -21,13 +20,13 @@ export default function BatchManager() {
} catch (err) { } catch (err) {
setError(err.message) setError(err.message)
} finally { } finally {
setLoading(false) if (showLoading) setLoading(false)
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
fetchTasks() fetchTasks(true)
const interval = setInterval(fetchTasks, 5000) const interval = setInterval(() => fetchTasks(false), 5000)
return () => clearInterval(interval) return () => clearInterval(interval)
}, [fetchTasks]) }, [fetchTasks])
@ -36,7 +35,7 @@ export default function BatchManager() {
const res = await fetch(`/api/analysis/cancel/${taskId}`, { method: 'DELETE' }) const res = await fetch(`/api/analysis/cancel/${taskId}`, { method: 'DELETE' })
if (!res.ok) throw new Error('取消失败') if (!res.ok) throw new Error('取消失败')
message.success('任务已取消') message.success('任务已取消')
fetchTasks() fetchTasks(false)
} catch (err) { } catch (err) {
message.error(err.message) message.error(err.message)
} }
@ -53,7 +52,7 @@ export default function BatchManager() {
}) })
if (!res.ok) throw new Error('重试失败') if (!res.ok) throw new Error('重试失败')
message.success('任务已重新提交') message.success('任务已重新提交')
fetchTasks() fetchTasks(false)
} catch (err) { } catch (err) {
message.error(err.message) message.error(err.message)
} }
@ -67,50 +66,16 @@ export default function BatchManager() {
}) })
} }
const getStatusIcon = (status) => { const columns = useMemo(() => [
switch (status) {
case 'completed':
return <CheckCircleOutlined style={{ color: 'var(--buy)', fontSize: 16 }} />
case 'failed':
return <CloseCircleOutlined style={{ color: 'var(--sell)', fontSize: 16 }} />
case 'running':
return <SyncOutlined spin style={{ color: 'var(--running)', fontSize: 16 }} />
default:
return <span style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid var(--border-strong)', display: 'inline-block' }} />
}
}
const getStatusTag = (status) => {
const map = {
pending: { text: '等待', bg: 'var(--bg-elevated)', color: 'var(--text-muted)' },
running: { text: '分析中', bg: 'var(--running-dim)', color: 'var(--running)' },
completed: { text: '完成', bg: 'var(--buy-dim)', color: 'var(--buy)' },
failed: { text: '失败', bg: 'var(--sell-dim)', color: 'var(--sell)' },
}
const s = map[status] || map.pending
return (
<span style={{ background: s.bg, color: s.color, padding: '2px 10px', borderRadius: 'var(--radius-pill)', fontSize: 12, fontWeight: 600 }}>
{s.text}
</span>
)
}
const getDecisionBadge = (decision) => {
if (!decision) return null
const cls = decision === 'BUY' ? 'badge-buy' : decision === 'SELL' ? 'badge-sell' : 'badge-hold'
return <span className={cls}>{decision}</span>
}
const columns = [
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
width: 110, width: 110,
render: (_, record) => ( render: (_, record) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <>
{getStatusIcon(record.status)} <StatusIcon status={record.status} />
{getStatusTag(record.status)} <StatusTag status={record.status} />
</div> </>
), ),
}, },
{ {
@ -143,7 +108,7 @@ export default function BatchManager() {
dataIndex: 'decision', dataIndex: 'decision',
key: 'decision', key: 'decision',
width: 80, width: 80,
render: getDecisionBadge, render: (decision) => <DecisionBadge decision={decision} />,
}, },
{ {
title: '任务ID', title: '任务ID',
@ -174,7 +139,7 @@ export default function BatchManager() {
render: (error) => render: (error) =>
error ? ( error ? (
<Tooltip title={error} placement="topLeft"> <Tooltip title={error} placement="topLeft">
<span style={{ color: 'var(--color-sell)', fontSize: 12, display: 'block' }}>{error}</span> <span style={{ color: 'var(--sell)', fontSize: 12, display: 'block' }}>{error}</span>
</Tooltip> </Tooltip>
) : null, ) : null,
}, },
@ -204,16 +169,18 @@ export default function BatchManager() {
</div> </div>
), ),
}, },
] ], [tasks]) // eslint-disable-line react-hooks/exhaustive-deps
const pendingCount = tasks.filter(t => t.status === 'pending').length const stats = useMemo(() => ({
const runningCount = tasks.filter(t => t.status === 'running').length pending: tasks.filter(t => t.status === 'pending').length,
const completedCount = tasks.filter(t => t.status === 'completed').length running: tasks.filter(t => t.status === 'running').length,
const failedCount = tasks.filter(t => t.status === 'failed').length completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
}), [tasks])
return ( return (
<div> <div>
{/* Compact stat strip — no card nesting, left-aligned with colored accents */} {/* Compact stat strip */}
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)', gridTemplateColumns: 'repeat(4, 1fr)',
@ -224,12 +191,12 @@ export default function BatchManager() {
padding: 'var(--space-1)', padding: 'var(--space-1)',
}}> }}>
{[ {[
{ label: '等待中', value: pendingCount, color: 'var(--text-muted)', border: 'var(--text-muted)' }, { label: '等待中', key: 'pending', color: 'var(--text-muted)', border: 'var(--text-muted)' },
{ label: '分析中', value: runningCount, color: 'var(--running)', border: 'var(--running)' }, { label: '分析中', key: 'running', color: 'var(--running)', border: 'var(--running)' },
{ label: '已完成', value: completedCount, color: 'var(--buy)', border: 'var(--buy)' }, { label: '已完成', key: 'completed', color: 'var(--buy)', border: 'var(--buy)' },
{ label: '失败', value: failedCount, color: 'var(--sell)', border: 'var(--sell)' }, { label: '失败', key: 'failed', color: 'var(--sell)', border: 'var(--sell)' },
].map(({ label, value, color, border }) => ( ].map(({ label, key, color, border }) => (
<div key={label} style={{ <div key={key} style={{
background: 'var(--bg-surface)', background: 'var(--bg-surface)',
borderRadius: 'var(--radius-md)', borderRadius: 'var(--radius-md)',
padding: 'var(--space-3) var(--space-4)', padding: 'var(--space-3) var(--space-4)',
@ -239,7 +206,7 @@ export default function BatchManager() {
}}> }}>
<div style={{ width: 3, height: 32, background: border, borderRadius: 2, flexShrink: 0 }} /> <div style={{ width: 3, height: 32, background: border, borderRadius: 2, flexShrink: 0 }} />
<div> <div>
<div className="text-data" style={{ fontSize: 22, fontWeight: 600, color, lineHeight: 1 }}>{value}</div> <div className="text-data" style={{ fontSize: 22, fontWeight: 600, color, lineHeight: 1 }}>{stats[key]}</div>
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--text-muted)', marginTop: 2 }}>{label}</div> <div style={{ fontSize: 'var(--text-xs)', color: 'var(--text-muted)', marginTop: 2 }}>{label}</div>
</div> </div>
</div> </div>
@ -258,7 +225,7 @@ export default function BatchManager() {
title="加载失败" title="加载失败"
subTitle={error} subTitle={error}
extra={ extra={
<Button type="primary" onClick={fetchTasks}> <Button type="primary" onClick={() => fetchTasks(true)}>
重试 重试
</Button> </Button>
} }

View File

@ -1,18 +1,18 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { import {
Table, Button, Input, Select, Space, Row, Col, Card, Progress, Result, Table, Button, Input, Select, Space, Row, Col, Card, Progress,
message, Popconfirm, Modal, Tabs, Tag, Tooltip, Upload, Form, Typography, message, Popconfirm, Modal, Tabs, Tooltip, Form, Typography,
} from 'antd' } from 'antd'
import { import {
PlusOutlined, DeleteOutlined, PlayCircleOutlined, UploadOutlined, PlusOutlined, DeleteOutlined, PlayCircleOutlined, UploadOutlined,
DownloadOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, DownloadOutlined, SyncOutlined, AccountBookOutlined,
AccountBookOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { portfolioApi } from '../services/portfolioApi' import { portfolioApi } from '../services/portfolioApi'
import DecisionBadge from '../components/DecisionBadge'
const { Text } = Typography const { Text } = Typography
// ============== Helpers ============== const DEFAULT_ACCOUNT = '默认账户'
const formatMoney = (v) => const formatMoney = (v) =>
v == null ? '—' : `¥${v.toFixed(2)}`; v == null ? '—' : `¥${v.toFixed(2)}`;
@ -20,12 +20,6 @@ const formatMoney = (v) =>
const formatPct = (v) => const formatPct = (v) =>
v == null ? '—' : `${v >= 0 ? '+' : ''}${v.toFixed(2)}%`; v == null ? '—' : `${v >= 0 ? '+' : ''}${v.toFixed(2)}%`;
const DecisionBadge = ({ decision }) => {
if (!decision) return null
const cls = decision === 'BUY' ? 'badge-buy' : decision === 'SELL' ? 'badge-sell' : 'badge-hold'
return <span className={cls}>{decision}</span>
}
// ============== Tab 1: Watchlist ============== // ============== Tab 1: Watchlist ==============
function WatchlistTab() { function WatchlistTab() {
@ -129,7 +123,7 @@ function WatchlistTab() {
function PositionsTab() { function PositionsTab() {
const [data, setData] = useState([]) const [data, setData] = useState([])
const [accounts, setAccounts] = useState(['默认账户']) const [accounts, setAccounts] = useState([DEFAULT_ACCOUNT])
const [account, setAccount] = useState(null) const [account, setAccount] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [addOpen, setAddOpen] = useState(false) const [addOpen, setAddOpen] = useState(false)
@ -143,7 +137,7 @@ function PositionsTab() {
portfolioApi.getAccounts(), portfolioApi.getAccounts(),
]) ])
setData(posRes.positions || []) setData(posRes.positions || [])
setAccounts(accRes.accounts || ['默认账户']) setAccounts(accRes.accounts || [DEFAULT_ACCOUNT])
} catch { } catch {
message.error('加载失败') message.error('加载失败')
} finally { } finally {
@ -155,7 +149,7 @@ function PositionsTab() {
const handleAdd = async (vals) => { const handleAdd = async (vals) => {
try { try {
await portfolioApi.addPosition({ ...vals, account: account || '默认账户' }) await portfolioApi.addPosition({ ...vals, account: account || DEFAULT_ACCOUNT })
message.success('已添加') message.success('已添加')
setAddOpen(false) setAddOpen(false)
form.resetFields() form.resetFields()
@ -187,7 +181,7 @@ function PositionsTab() {
} }
} }
const totalPnl = data.reduce((s, p) => s + (p.unrealized_pnl || 0), 0) const totalPnl = useMemo(() => data.reduce((s, p) => s + (p.unrealized_pnl || 0), 0), [data])
const columns = [ const columns = [
{ title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110, { title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110,
@ -342,11 +336,19 @@ function RecommendationsTab() {
ws.onopen = () => setWsConnected(true) ws.onopen = () => setWsConnected(true)
ws.onmessage = (e) => { ws.onmessage = (e) => {
const d = JSON.parse(e.data) const d = JSON.parse(e.data)
if (d.type === 'progress') setProgress(d) if (d.type === 'progress') {
setProgress(d)
if (d.status === 'completed' || d.status === 'failed') {
setAnalyzing(false)
setTaskId(null)
setProgress(null)
fetchRecs(selectedDate)
}
}
} }
ws.onclose = () => setWsConnected(false) ws.onclose = () => setWsConnected(false)
wsRef.current = ws wsRef.current = ws
}, []) }, [fetchRecs, selectedDate])
const handleAnalyze = async () => { const handleAnalyze = async () => {
try { try {
@ -362,15 +364,13 @@ function RecommendationsTab() {
} }
useEffect(() => { useEffect(() => {
if (progress?.status === 'completed' || progress?.status === 'failed') { return () => {
setAnalyzing(false) if (wsRef.current) {
setTaskId(null) wsRef.current.close()
setProgress(null) wsRef.current = null
fetchRecs(selectedDate) }
} }
}, [progress?.status]) }, [])
useEffect(() => () => { if (wsRef.current) wsRef.current.close() }, [])
const columns = [ const columns = [
{ title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110, { title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110,

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useMemo } from 'react'
import { Table, Input, Modal, Skeleton, Button, Space, message } from 'antd' import { Table, Input, Modal, Skeleton, Button, Space, message } from 'antd'
import { FileTextOutlined, SearchOutlined, CloseOutlined, DownloadOutlined } from '@ant-design/icons' import { FileTextOutlined, SearchOutlined, CloseOutlined, DownloadOutlined } from '@ant-design/icons'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
@ -74,13 +74,16 @@ export default function ReportsViewer() {
} }
} }
const filteredReports = reports.filter( const filteredReports = useMemo(() =>
(r) => reports.filter(
r.ticker.toLowerCase().includes(searchText.toLowerCase()) || (r) =>
r.date.includes(searchText) r.ticker.toLowerCase().includes(searchText.toLowerCase()) ||
r.date.includes(searchText)
),
[reports, searchText]
) )
const columns = [ const columns = useMemo(() => [
{ {
title: '代码', title: '代码',
dataIndex: 'ticker', dataIndex: 'ticker',
@ -114,7 +117,7 @@ export default function ReportsViewer() {
</Button> </Button>
), ),
}, },
] ], [])
return ( return (
<div> <div>

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Table, Button, Select, Space, Row, Col, Skeleton, Result, message, Popconfirm, Tooltip } from 'antd' import { Table, Button, Select, Space, Row, Col, Skeleton, Result, message, Popconfirm, Tooltip } from 'antd'
import { PlayCircleOutlined, ReloadOutlined, QuestionCircleOutlined } from '@ant-design/icons' import { PlayCircleOutlined, ReloadOutlined, QuestionCircleOutlined } from '@ant-design/icons'
@ -11,6 +11,9 @@ const SCREEN_MODES = [
{ value: 'fundamentals_only', label: '纯基本面 (Fundamentals Only)' }, { value: 'fundamentals_only', label: '纯基本面 (Fundamentals Only)' },
] ]
const HEADER_LABEL_STYLE = { display: 'inline-flex', alignItems: 'center', gap: 4 }
const HEADER_ICON_STYLE = { fontSize: 10, color: 'var(--text-muted)' }
export default function ScreeningPanel() { export default function ScreeningPanel() {
const navigate = useNavigate() const navigate = useNavigate()
const [mode, setMode] = useState('china_strict') const [mode, setMode] = useState('china_strict')
@ -56,7 +59,7 @@ export default function ScreeningPanel() {
} }
} }
const columns = [ const columns = useMemo(() => [
{ {
title: '代码', title: '代码',
dataIndex: 'ticker', dataIndex: 'ticker',
@ -78,8 +81,8 @@ export default function ScreeningPanel() {
{ {
title: ( title: (
<Tooltip title="营业收入同比增长率"> <Tooltip title="营业收入同比增长率">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}> <span style={HEADER_LABEL_STYLE}>
营收增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} /> 营收增速 <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
</span> </span>
</Tooltip> </Tooltip>
), ),
@ -88,7 +91,7 @@ export default function ScreeningPanel() {
align: 'right', align: 'right',
width: 100, width: 100,
render: (val) => ( render: (val) => (
<span className="text-data" style={{ color: val > 0 ? 'var(--color-buy)' : 'var(--color-sell)' }}> <span className="text-data" style={{ color: val > 0 ? 'var(--buy)' : 'var(--sell)' }}>
{val?.toFixed(1)}% {val?.toFixed(1)}%
</span> </span>
), ),
@ -96,8 +99,8 @@ export default function ScreeningPanel() {
{ {
title: ( title: (
<Tooltip title="净利润同比增长率"> <Tooltip title="净利润同比增长率">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}> <span style={HEADER_LABEL_STYLE}>
利润增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} /> 利润增速 <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
</span> </span>
</Tooltip> </Tooltip>
), ),
@ -106,7 +109,7 @@ export default function ScreeningPanel() {
align: 'right', align: 'right',
width: 100, width: 100,
render: (val) => ( render: (val) => (
<span className="text-data" style={{ color: val > 0 ? 'var(--color-buy)' : 'var(--color-sell)' }}> <span className="text-data" style={{ color: val > 0 ? 'var(--buy)' : 'var(--sell)' }}>
{val?.toFixed(1)}% {val?.toFixed(1)}%
</span> </span>
), ),
@ -114,8 +117,8 @@ export default function ScreeningPanel() {
{ {
title: ( title: (
<Tooltip title="净资产收益率 = 净利润/净资产"> <Tooltip title="净资产收益率 = 净利润/净资产">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}> <span style={HEADER_LABEL_STYLE}>
ROE <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} /> ROE <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
</span> </span>
</Tooltip> </Tooltip>
), ),
@ -140,8 +143,8 @@ export default function ScreeningPanel() {
{ {
title: ( title: (
<Tooltip title="当前成交量/过去20日平均成交量"> <Tooltip title="当前成交量/过去20日平均成交量">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}> <span style={HEADER_LABEL_STYLE}>
Vol比 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} /> Vol比 <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
</span> </span>
</Tooltip> </Tooltip>
), ),
@ -175,7 +178,7 @@ export default function ScreeningPanel() {
</Popconfirm> </Popconfirm>
), ),
}, },
] ], [])
return ( return (
<div> <div>