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:
parent
5c4d0a72fc
commit
dd9392c9fb
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
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 = [
|
||||
{ key: 'analysts', label: '分析师团队' },
|
||||
|
|
@ -79,31 +80,6 @@ export default function AnalysisMonitor() {
|
|||
}
|
||||
}, [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) {
|
||||
return (
|
||||
<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 }}>
|
||||
{task.ticker}
|
||||
</span>
|
||||
{getDecisionBadge(task.decision)}
|
||||
<DecisionBadge decision={task.decision} />
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
|
|
@ -200,7 +176,7 @@ export default function AnalysisMonitor() {
|
|||
const status = stageState?.status || 'pending'
|
||||
return (
|
||||
<div key={stage.key} className={`stage-pill ${status}`}>
|
||||
{getStageIcon(status)}
|
||||
<StatusIcon status={status} />
|
||||
<span>{stage.label}</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Table, Button, Progress, Result, Empty, Card, message, Popconfirm, Tooltip } from 'antd'
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, SyncOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
|
||||
const MAX_CONCURRENT = 3
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { Table, Button, Progress, Result, Card, message, Popconfirm, Tooltip } from 'antd'
|
||||
import { DeleteOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import DecisionBadge from '../components/DecisionBadge'
|
||||
import { StatusIcon, StatusTag } from '../components/StatusIcon'
|
||||
|
||||
export default function BatchManager() {
|
||||
const [tasks, setTasks] = useState([])
|
||||
const [maxConcurrent] = useState(MAX_CONCURRENT)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
setLoading(true)
|
||||
const fetchTasks = useCallback(async (showLoading = true) => {
|
||||
if (showLoading) setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/analysis/tasks')
|
||||
if (!res.ok) throw new Error('获取任务列表失败')
|
||||
|
|
@ -21,13 +20,13 @@ export default function BatchManager() {
|
|||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (showLoading) setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks()
|
||||
const interval = setInterval(fetchTasks, 5000)
|
||||
fetchTasks(true)
|
||||
const interval = setInterval(() => fetchTasks(false), 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchTasks])
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ export default function BatchManager() {
|
|||
const res = await fetch(`/api/analysis/cancel/${taskId}`, { method: 'DELETE' })
|
||||
if (!res.ok) throw new Error('取消失败')
|
||||
message.success('任务已取消')
|
||||
fetchTasks()
|
||||
fetchTasks(false)
|
||||
} catch (err) {
|
||||
message.error(err.message)
|
||||
}
|
||||
|
|
@ -53,7 +52,7 @@ export default function BatchManager() {
|
|||
})
|
||||
if (!res.ok) throw new Error('重试失败')
|
||||
message.success('任务已重新提交')
|
||||
fetchTasks()
|
||||
fetchTasks(false)
|
||||
} catch (err) {
|
||||
message.error(err.message)
|
||||
}
|
||||
|
|
@ -67,50 +66,16 @@ export default function BatchManager() {
|
|||
})
|
||||
}
|
||||
|
||||
const getStatusIcon = (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' }} />
|
||||
}
|
||||
}
|
||||
|
||||
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 = [
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{getStatusIcon(record.status)}
|
||||
{getStatusTag(record.status)}
|
||||
</div>
|
||||
<>
|
||||
<StatusIcon status={record.status} />
|
||||
<StatusTag status={record.status} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -143,7 +108,7 @@ export default function BatchManager() {
|
|||
dataIndex: 'decision',
|
||||
key: 'decision',
|
||||
width: 80,
|
||||
render: getDecisionBadge,
|
||||
render: (decision) => <DecisionBadge decision={decision} />,
|
||||
},
|
||||
{
|
||||
title: '任务ID',
|
||||
|
|
@ -174,7 +139,7 @@ export default function BatchManager() {
|
|||
render: (error) =>
|
||||
error ? (
|
||||
<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>
|
||||
) : null,
|
||||
},
|
||||
|
|
@ -204,16 +169,18 @@ export default function BatchManager() {
|
|||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
], [tasks]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const pendingCount = tasks.filter(t => t.status === 'pending').length
|
||||
const runningCount = tasks.filter(t => t.status === 'running').length
|
||||
const completedCount = tasks.filter(t => t.status === 'completed').length
|
||||
const failedCount = tasks.filter(t => t.status === 'failed').length
|
||||
const stats = useMemo(() => ({
|
||||
pending: tasks.filter(t => t.status === 'pending').length,
|
||||
running: tasks.filter(t => t.status === 'running').length,
|
||||
completed: tasks.filter(t => t.status === 'completed').length,
|
||||
failed: tasks.filter(t => t.status === 'failed').length,
|
||||
}), [tasks])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Compact stat strip — no card nesting, left-aligned with colored accents */}
|
||||
{/* Compact stat strip */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
|
|
@ -224,12 +191,12 @@ export default function BatchManager() {
|
|||
padding: 'var(--space-1)',
|
||||
}}>
|
||||
{[
|
||||
{ label: '等待中', value: pendingCount, color: 'var(--text-muted)', border: 'var(--text-muted)' },
|
||||
{ label: '分析中', value: runningCount, color: 'var(--running)', border: 'var(--running)' },
|
||||
{ label: '已完成', value: completedCount, color: 'var(--buy)', border: 'var(--buy)' },
|
||||
{ label: '失败', value: failedCount, color: 'var(--sell)', border: 'var(--sell)' },
|
||||
].map(({ label, value, color, border }) => (
|
||||
<div key={label} style={{
|
||||
{ label: '等待中', key: 'pending', color: 'var(--text-muted)', border: 'var(--text-muted)' },
|
||||
{ label: '分析中', key: 'running', color: 'var(--running)', border: 'var(--running)' },
|
||||
{ label: '已完成', key: 'completed', color: 'var(--buy)', border: 'var(--buy)' },
|
||||
{ label: '失败', key: 'failed', color: 'var(--sell)', border: 'var(--sell)' },
|
||||
].map(({ label, key, color, border }) => (
|
||||
<div key={key} style={{
|
||||
background: 'var(--bg-surface)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
|
|
@ -258,7 +225,7 @@ export default function BatchManager() {
|
|||
title="加载失败"
|
||||
subTitle={error}
|
||||
extra={
|
||||
<Button type="primary" onClick={fetchTasks}>
|
||||
<Button type="primary" onClick={() => fetchTasks(true)}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||
import {
|
||||
Table, Button, Input, Select, Space, Row, Col, Card, Progress, Result,
|
||||
message, Popconfirm, Modal, Tabs, Tag, Tooltip, Upload, Form, Typography,
|
||||
Table, Button, Input, Select, Space, Row, Col, Card, Progress,
|
||||
message, Popconfirm, Modal, Tabs, Tooltip, Form, Typography,
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined, DeleteOutlined, PlayCircleOutlined, UploadOutlined,
|
||||
DownloadOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined,
|
||||
AccountBookOutlined,
|
||||
DownloadOutlined, SyncOutlined, AccountBookOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { portfolioApi } from '../services/portfolioApi'
|
||||
import DecisionBadge from '../components/DecisionBadge'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
// ============== Helpers ==============
|
||||
const DEFAULT_ACCOUNT = '默认账户'
|
||||
|
||||
const formatMoney = (v) =>
|
||||
v == null ? '—' : `¥${v.toFixed(2)}`;
|
||||
|
|
@ -20,12 +20,6 @@ const formatMoney = (v) =>
|
|||
const formatPct = (v) =>
|
||||
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 ==============
|
||||
|
||||
function WatchlistTab() {
|
||||
|
|
@ -129,7 +123,7 @@ function WatchlistTab() {
|
|||
|
||||
function PositionsTab() {
|
||||
const [data, setData] = useState([])
|
||||
const [accounts, setAccounts] = useState(['默认账户'])
|
||||
const [accounts, setAccounts] = useState([DEFAULT_ACCOUNT])
|
||||
const [account, setAccount] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
|
|
@ -143,7 +137,7 @@ function PositionsTab() {
|
|||
portfolioApi.getAccounts(),
|
||||
])
|
||||
setData(posRes.positions || [])
|
||||
setAccounts(accRes.accounts || ['默认账户'])
|
||||
setAccounts(accRes.accounts || [DEFAULT_ACCOUNT])
|
||||
} catch {
|
||||
message.error('加载失败')
|
||||
} finally {
|
||||
|
|
@ -155,7 +149,7 @@ function PositionsTab() {
|
|||
|
||||
const handleAdd = async (vals) => {
|
||||
try {
|
||||
await portfolioApi.addPosition({ ...vals, account: account || '默认账户' })
|
||||
await portfolioApi.addPosition({ ...vals, account: account || DEFAULT_ACCOUNT })
|
||||
message.success('已添加')
|
||||
setAddOpen(false)
|
||||
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 = [
|
||||
{ title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110,
|
||||
|
|
@ -342,11 +336,19 @@ function RecommendationsTab() {
|
|||
ws.onopen = () => setWsConnected(true)
|
||||
ws.onmessage = (e) => {
|
||||
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)
|
||||
wsRef.current = ws
|
||||
}, [])
|
||||
}, [fetchRecs, selectedDate])
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
try {
|
||||
|
|
@ -362,15 +364,13 @@ function RecommendationsTab() {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (progress?.status === 'completed' || progress?.status === 'failed') {
|
||||
setAnalyzing(false)
|
||||
setTaskId(null)
|
||||
setProgress(null)
|
||||
fetchRecs(selectedDate)
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close()
|
||||
wsRef.current = null
|
||||
}
|
||||
}
|
||||
}, [progress?.status])
|
||||
|
||||
useEffect(() => () => { if (wsRef.current) wsRef.current.close() }, [])
|
||||
}, [])
|
||||
|
||||
const columns = [
|
||||
{ title: '代码', dataIndex: 'ticker', key: 'ticker', width: 110,
|
||||
|
|
|
|||
|
|
@ -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 { FileTextOutlined, SearchOutlined, CloseOutlined, DownloadOutlined } from '@ant-design/icons'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
|
@ -74,13 +74,16 @@ export default function ReportsViewer() {
|
|||
}
|
||||
}
|
||||
|
||||
const filteredReports = reports.filter(
|
||||
(r) =>
|
||||
r.ticker.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
r.date.includes(searchText)
|
||||
const filteredReports = useMemo(() =>
|
||||
reports.filter(
|
||||
(r) =>
|
||||
r.ticker.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
r.date.includes(searchText)
|
||||
),
|
||||
[reports, searchText]
|
||||
)
|
||||
|
||||
const columns = [
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '代码',
|
||||
dataIndex: 'ticker',
|
||||
|
|
@ -114,7 +117,7 @@ export default function ReportsViewer() {
|
|||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
], [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Table, Button, Select, Space, Row, Col, Skeleton, Result, message, Popconfirm, Tooltip } from 'antd'
|
||||
import { PlayCircleOutlined, ReloadOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
|
|
@ -11,6 +11,9 @@ const SCREEN_MODES = [
|
|||
{ 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() {
|
||||
const navigate = useNavigate()
|
||||
const [mode, setMode] = useState('china_strict')
|
||||
|
|
@ -56,7 +59,7 @@ export default function ScreeningPanel() {
|
|||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '代码',
|
||||
dataIndex: 'ticker',
|
||||
|
|
@ -78,8 +81,8 @@ export default function ScreeningPanel() {
|
|||
{
|
||||
title: (
|
||||
<Tooltip title="营业收入同比增长率">
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
营收增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
|
||||
<span style={HEADER_LABEL_STYLE}>
|
||||
营收增速 <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
|
|
@ -88,7 +91,7 @@ export default function ScreeningPanel() {
|
|||
align: 'right',
|
||||
width: 100,
|
||||
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)}%
|
||||
</span>
|
||||
),
|
||||
|
|
@ -96,8 +99,8 @@ export default function ScreeningPanel() {
|
|||
{
|
||||
title: (
|
||||
<Tooltip title="净利润同比增长率">
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
利润增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
|
||||
<span style={HEADER_LABEL_STYLE}>
|
||||
利润增速 <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
|
|
@ -106,7 +109,7 @@ export default function ScreeningPanel() {
|
|||
align: 'right',
|
||||
width: 100,
|
||||
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)}%
|
||||
</span>
|
||||
),
|
||||
|
|
@ -114,8 +117,8 @@ export default function ScreeningPanel() {
|
|||
{
|
||||
title: (
|
||||
<Tooltip title="净资产收益率 = 净利润/净资产">
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
ROE <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
|
||||
<span style={HEADER_LABEL_STYLE}>
|
||||
ROE <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
|
|
@ -140,8 +143,8 @@ export default function ScreeningPanel() {
|
|||
{
|
||||
title: (
|
||||
<Tooltip title="当前成交量/过去20日平均成交量">
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
Vol比 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
|
||||
<span style={HEADER_LABEL_STYLE}>
|
||||
Vol比 <QuestionCircleOutlined style={HEADER_ICON_STYLE} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
|
|
@ -175,7 +178,7 @@ export default function ScreeningPanel() {
|
|||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]
|
||||
], [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue