diff --git a/web_dashboard/frontend/src/components/DecisionBadge.jsx b/web_dashboard/frontend/src/components/DecisionBadge.jsx
new file mode 100644
index 00000000..d5ebf8bf
--- /dev/null
+++ b/web_dashboard/frontend/src/components/DecisionBadge.jsx
@@ -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 {decision}
+}
diff --git a/web_dashboard/frontend/src/components/StatusIcon.jsx b/web_dashboard/frontend/src/components/StatusIcon.jsx
new file mode 100644
index 00000000..696056af
--- /dev/null
+++ b/web_dashboard/frontend/src/components/StatusIcon.jsx
@@ -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
+ case 'failed':
+ return
+ case 'running':
+ return
+ default:
+ return (
+
+ )
+ }
+}
+
+export function StatusTag({ status }) {
+ const s = STATUS_TAG_MAP[status] || STATUS_TAG_MAP.pending
+ return (
+
+ {s.text}
+
+ )
+}
diff --git a/web_dashboard/frontend/src/pages/AnalysisMonitor.jsx b/web_dashboard/frontend/src/pages/AnalysisMonitor.jsx
index beba4760..f91cc27e 100644
--- a/web_dashboard/frontend/src/pages/AnalysisMonitor.jsx
+++ b/web_dashboard/frontend/src/pages/AnalysisMonitor.jsx
@@ -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
- case 'running':
- return
- case 'failed':
- return
- default:
- return
- }
- }
-
- const getDecisionBadge = (decision) => {
- if (!decision) return null
- const badgeClass = decision === 'BUY' ? 'badge-buy' : decision === 'SELL' ? 'badge-sell' : 'badge-hold'
- return {decision}
- }
-
if (!taskId) {
return (
@@ -179,7 +155,7 @@ export default function AnalysisMonitor() {
{task.ticker}
- {getDecisionBadge(task.decision)}
+
{/* Progress */}
@@ -200,7 +176,7 @@ export default function AnalysisMonitor() {
const status = stageState?.status || 'pending'
return (
- {getStageIcon(status)}
+
{stage.label}
)
diff --git a/web_dashboard/frontend/src/pages/BatchManager.jsx b/web_dashboard/frontend/src/pages/BatchManager.jsx
index 084a18fe..22098670 100644
--- a/web_dashboard/frontend/src/pages/BatchManager.jsx
+++ b/web_dashboard/frontend/src/pages/BatchManager.jsx
@@ -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
- case 'failed':
- return
- case 'running':
- return
- default:
- return
- }
- }
-
- 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 (
-
- {s.text}
-
- )
- }
-
- const getDecisionBadge = (decision) => {
- if (!decision) return null
- const cls = decision === 'BUY' ? 'badge-buy' : decision === 'SELL' ? 'badge-sell' : 'badge-hold'
- return {decision}
- }
-
- const columns = [
+ const columns = useMemo(() => [
{
title: '状态',
key: 'status',
width: 110,
render: (_, record) => (
-
- {getStatusIcon(record.status)}
- {getStatusTag(record.status)}
-
+ <>
+
+
+ >
),
},
{
@@ -143,7 +108,7 @@ export default function BatchManager() {
dataIndex: 'decision',
key: 'decision',
width: 80,
- render: getDecisionBadge,
+ render: (decision) => ,
},
{
title: '任务ID',
@@ -174,7 +139,7 @@ export default function BatchManager() {
render: (error) =>
error ? (
- {error}
+ {error}
) : null,
},
@@ -204,16 +169,18 @@ export default function BatchManager() {
),
},
- ]
+ ], [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 (
- {/* Compact stat strip — no card nesting, left-aligned with colored accents */}
+ {/* Compact stat strip */}
{[
- { 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 }) => (
-
(
+
-
{value}
+
{stats[key]}
{label}
@@ -258,7 +225,7 @@ export default function BatchManager() {
title="加载失败"
subTitle={error}
extra={
-
),
},
- ]
+ ], [])
return (
diff --git a/web_dashboard/frontend/src/pages/ScreeningPanel.jsx b/web_dashboard/frontend/src/pages/ScreeningPanel.jsx
index b0ba1413..453245aa 100644
--- a/web_dashboard/frontend/src/pages/ScreeningPanel.jsx
+++ b/web_dashboard/frontend/src/pages/ScreeningPanel.jsx
@@ -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: (
-
- 营收增速
+
+ 营收增速
),
@@ -88,7 +91,7 @@ export default function ScreeningPanel() {
align: 'right',
width: 100,
render: (val) => (
-
0 ? 'var(--color-buy)' : 'var(--color-sell)' }}>
+ 0 ? 'var(--buy)' : 'var(--sell)' }}>
{val?.toFixed(1)}%
),
@@ -96,8 +99,8 @@ export default function ScreeningPanel() {
{
title: (
-
- 利润增速
+
+ 利润增速
),
@@ -106,7 +109,7 @@ export default function ScreeningPanel() {
align: 'right',
width: 100,
render: (val) => (
- 0 ? 'var(--color-buy)' : 'var(--color-sell)' }}>
+ 0 ? 'var(--buy)' : 'var(--sell)' }}>
{val?.toFixed(1)}%
),
@@ -114,8 +117,8 @@ export default function ScreeningPanel() {
{
title: (
-
- ROE
+
+ ROE
),
@@ -140,8 +143,8 @@ export default function ScreeningPanel() {
{
title: (
-
- Vol比
+
+ Vol比
),
@@ -175,7 +178,7 @@ export default function ScreeningPanel() {
),
},
- ]
+ ], [])
return (