feat(dashboard): dark terminal design system overhaul

Complete visual redesign replacing Apple glassmorphism with Bloomberg-style
dark trading terminal aesthetic:

- Dark palette: #0d0d0f base, cyan accent (#00d4ff), green/red/amber signals
- Font pair: DM Sans (UI) + JetBrains Mono (data/numbers)
- Solid sidebar (no backdrop-filter blur)
- Compact stat strip in BatchManager (replaces 4-card hero row)
- Color system: semantic buy/sell/hold/running with CSS variables
- All inline rgba(0,0,0,...) → dark theme tokens
- All var(--font-*) → font-ui / font-data
- Focus-visible outlines on all interactive elements
- prefers-reduced-motion support
- Emoji status indicators → CSS status-dot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
陈少杰 2026-04-07 20:05:16 +08:00
parent 045b61fd7e
commit f196962112
6 changed files with 457 additions and 736 deletions

File diff suppressed because it is too large Load Diff

View File

@ -94,7 +94,7 @@ export default function AnalysisMonitor() {
case 'failed':
return <CloseCircleOutlined style={{ color: 'var(--color-sell)', fontSize: 16 }} />
default:
return <span style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid rgba(0,0,0,0.12)', display: 'inline-block' }} />
return <span style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid var(--border-strong)', display: 'inline-block' }} />
}
}
@ -136,13 +136,13 @@ export default function AnalysisMonitor() {
style={{ marginBottom: 'var(--space-6)' }}
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontFamily: 'var(--font-display)', fontSize: 17, fontWeight: 600 }}>
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 17, fontWeight: 600 }}>
当前分析任务
</span>
<Badge
status={error ? 'error' : wsConnected ? 'success' : 'default'}
text={
<span style={{ fontSize: 12, color: error ? 'var(--color-sell)' : wsConnected ? 'var(--color-buy)' : 'rgba(0,0,0,0.48)' }}>
<span style={{ fontSize: 12, color: error ? 'var(--sell)' : wsConnected ? 'var(--buy)' : 'var(--text-muted)' }}>
{error ? '错误' : wsConnected ? '实时连接' : '连接中'}
</span>
}
@ -176,7 +176,7 @@ export default function AnalysisMonitor() {
{/* Task Header */}
<div style={{ marginBottom: 'var(--space-6)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}>
<span style={{ fontFamily: 'var(--font-display)', 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}
</span>
{getDecisionBadge(task.decision)}
@ -216,7 +216,7 @@ export default function AnalysisMonitor() {
style={{
fontFamily: 'var(--font-data)',
fontSize: 12,
background: 'rgba(0,0,0,0.03)',
background: 'var(--bg-elevated)',
padding: 'var(--space-4)',
borderRadius: 'var(--radius-standard)',
maxHeight: 280,
@ -226,13 +226,13 @@ export default function AnalysisMonitor() {
{task.logs?.length > 0 ? (
task.logs.map((log, i) => (
<div key={i} style={{ marginBottom: 8, lineHeight: 1.4 }}>
<span style={{ color: 'rgba(0,0,0,0.48)' }}>[{log.time}]</span>{' '}
<span style={{ color: 'var(--text-muted)' }}>[{log.time}]</span>{' '}
<span style={{ fontWeight: 500 }}>{log.stage}:</span>{' '}
<span>{log.message}</span>
</div>
))
) : (
<div style={{ color: 'rgba(0,0,0,0.48)', textAlign: 'center', padding: 'var(--space-4)' }}>
<div style={{ color: 'var(--text-muted)', textAlign: 'center', padding: 'var(--space-4)' }}>
等待日志输出...
</div>
)}

View File

@ -70,22 +70,22 @@ export default function BatchManager() {
const getStatusIcon = (status) => {
switch (status) {
case 'completed':
return <CheckCircleOutlined style={{ color: 'var(--color-buy)', fontSize: 16 }} />
return <CheckCircleOutlined style={{ color: 'var(--buy)', fontSize: 16 }} />
case 'failed':
return <CloseCircleOutlined style={{ color: 'var(--color-sell)', fontSize: 16 }} />
return <CloseCircleOutlined style={{ color: 'var(--sell)', fontSize: 16 }} />
case 'running':
return <SyncOutlined spin style={{ color: 'var(--color-running)', fontSize: 16 }} />
return <SyncOutlined spin style={{ color: 'var(--running)', fontSize: 16 }} />
default:
return <span style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid rgba(0,0,0,0.2)', display: 'inline-block' }} />
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: 'rgba(0,0,0,0.06)', color: 'rgba(0,0,0,0.48)' },
running: { text: '分析中', bg: 'rgba(168,85,247,0.12)', color: 'var(--color-running)' },
completed: { text: '完成', bg: 'rgba(34,197,94,0.12)', color: 'var(--color-buy)' },
failed: { text: '失败', bg: 'rgba(220,38,38,0.12)', color: 'var(--color-sell)' },
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 (
@ -118,7 +118,7 @@ export default function BatchManager() {
dataIndex: 'ticker',
key: 'ticker',
render: (text) => (
<span style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 15 }}>{text}</span>
<span style={{ fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 15 }}>{text}</span>
),
},
{
@ -131,8 +131,8 @@ export default function BatchManager() {
<Progress
percent={val || 0}
size="small"
strokeColor="var(--color-apple-blue)"
trailColor="rgba(0,0,0,0.08)"
strokeColor="var(--accent)"
trailColor="rgba(255,255,255,0.06)"
/>
) : (
<span className="text-data">{val || 0}%</span>
@ -152,12 +152,12 @@ export default function BatchManager() {
width: 220,
render: (text) => (
<Tooltip title={text} placement="topLeft">
<span className="text-data" style={{ fontSize: 11, color: 'rgba(0,0,0,0.48)', cursor: 'default' }}>
<span className="text-data" style={{ fontSize: 11, color: 'var(--text-muted)', cursor: 'default' }}>
{text.slice(0, 18)}...
</span>
<button
onClick={(e) => { e.stopPropagation(); handleCopyTaskId(text) }}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 4px', color: 'rgba(0,0,0,0.48)', display: 'inline-flex', alignItems: 'center' }}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px 4px', color: 'var(--text-muted)', display: 'inline-flex', alignItems: 'center' }}
title="复制任务ID"
>
<CopyOutlined style={{ fontSize: 12 }} />
@ -213,24 +213,37 @@ export default function BatchManager() {
return (
<div>
{/* Stats */}
<div style={{ display: 'flex', gap: 'var(--space-4)', marginBottom: 'var(--space-6)' }}>
<Card size="small" className="card" style={{ flex: 1 }}>
<div className="text-data" style={{ fontSize: 32, fontWeight: 600 }}>{pendingCount}</div>
<div className="text-caption">等待中</div>
</Card>
<Card size="small" className="card" style={{ flex: 1 }}>
<div className="text-data" style={{ fontSize: 32, fontWeight: 600, color: 'var(--color-running)' }}>{runningCount}</div>
<div className="text-caption">分析中</div>
</Card>
<Card size="small" className="card" style={{ flex: 1 }}>
<div className="text-data" style={{ fontSize: 32, fontWeight: 600, color: 'var(--color-buy)' }}>{completedCount}</div>
<div className="text-caption">已完成</div>
</Card>
<Card size="small" className="card" style={{ flex: 1 }}>
<div className="text-data" style={{ fontSize: 32, fontWeight: 600, color: 'var(--color-sell)' }}>{failedCount}</div>
<div className="text-caption">失败</div>
</Card>
{/* Compact stat strip — no card nesting, left-aligned with colored accents */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 'var(--space-1)',
marginBottom: 'var(--space-6)',
background: 'var(--border)',
borderRadius: 'var(--radius-lg)',
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={{
background: 'var(--bg-surface)',
borderRadius: 'var(--radius-md)',
padding: 'var(--space-3) var(--space-4)',
display: 'flex',
alignItems: 'center',
gap: 'var(--space-3)',
}}>
<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 style={{ fontSize: 'var(--text-xs)', color: 'var(--text-muted)', marginTop: 2 }}>{label}</div>
</div>
</div>
))}
</div>
{/* Tasks Table */}

View File

@ -272,7 +272,7 @@ function PositionsTab() {
<Table columns={columns} dataSource={data} rowKey="position_id" loading={loading} pagination={false} size="middle" scroll={{ x: 700 }} />
{data.length === 0 && !loading && (
<div className="empty-state">
<AccountBookOutlined style={{ fontSize: 40, color: 'rgba(0,0,0,0.2)' }} />
<AccountBookOutlined style={{ fontSize: 40, color: 'var(--text-muted)' }} />
<div className="empty-state-title">暂无持仓</div>
<div className="empty-state-description">点击"添加持仓"录入您的股票仓位</div>
</div>
@ -391,8 +391,8 @@ function RecommendationsTab() {
<div className="card-title">今日建议</div>
<Space>
{analyzing && progress && (
<span className="text-caption">
{wsConnected ? '🟢' : '🔴'}
<span className="text-caption" style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
<span className={`status-dot ${wsConnected ? 'connected' : 'error'}`} />
{progress.completed || 0} / {progress.total || 0}
</span>
)}
@ -411,7 +411,7 @@ function RecommendationsTab() {
<Progress
percent={Math.round(((progress.completed || 0) / (progress.total || 1)) * 100)}
status="active"
strokeColor="var(--color-apple-blue)"
strokeColor="var(--accent)"
/>
)}
</div>

View File

@ -87,7 +87,7 @@ export default function ReportsViewer() {
key: 'ticker',
width: 120,
render: (text) => (
<span style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 15 }}>{text}</span>
<span style={{ fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 15 }}>{text}</span>
),
},
{
@ -126,7 +126,7 @@ export default function ReportsViewer() {
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
prefix={<SearchOutlined style={{ color: 'rgba(0,0,0,0.48)' }} />}
prefix={<SearchOutlined style={{ color: 'var(--text-muted)' }} />}
size="large"
style={{ flex: 1 }}
/>
@ -169,10 +169,10 @@ export default function ReportsViewer() {
title={
selectedReport ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontFamily: 'var(--font-display)', fontSize: 17, fontWeight: 600 }}>
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 17, fontWeight: 600 }}>
{selectedReport.ticker}
</span>
<span style={{ color: 'rgba(0,0,0,0.48)', fontSize: 14 }}>{selectedReport.date}</span>
<span style={{ color: 'var(--text-muted)', fontSize: 14 }}>{selectedReport.date}</span>
</div>
) : null
}
@ -201,7 +201,7 @@ export default function ReportsViewer() {
styles={{
wrapper: { maxWidth: '95vw' },
body: { maxHeight: '70vh', overflow: 'auto', padding: 'var(--space-6)' },
header: { padding: 'var(--space-4) var(--space-6)', borderBottom: '1px solid rgba(0,0,0,0.08)' },
header: { padding: 'var(--space-4) var(--space-6)', borderBottom: '1px solid var(--border)' },
}}
>
{loadingContent ? (
@ -211,7 +211,7 @@ export default function ReportsViewer() {
) : reportContent ? (
<div
style={{
fontFamily: 'var(--font-text)',
fontFamily: 'var(--font-ui)',
lineHeight: 1.8,
fontSize: 15,
}}

View File

@ -79,7 +79,7 @@ export default function ScreeningPanel() {
title: (
<Tooltip title="营业收入同比增长率">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
营收增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'rgba(0,0,0,0.48)' }} />
营收增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
</span>
</Tooltip>
),
@ -97,7 +97,7 @@ export default function ScreeningPanel() {
title: (
<Tooltip title="净利润同比增长率">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
利润增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'rgba(0,0,0,0.48)' }} />
利润增速 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
</span>
</Tooltip>
),
@ -115,7 +115,7 @@ export default function ScreeningPanel() {
title: (
<Tooltip title="净资产收益率 = 净利润/净资产">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
ROE <QuestionCircleOutlined style={{ fontSize: 10, color: 'rgba(0,0,0,0.48)' }} />
ROE <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
</span>
</Tooltip>
),
@ -141,7 +141,7 @@ export default function ScreeningPanel() {
title: (
<Tooltip title="当前成交量/过去20日平均成交量">
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
Vol比 <QuestionCircleOutlined style={{ fontSize: 10, color: 'rgba(0,0,0,0.48)' }} />
Vol比 <QuestionCircleOutlined style={{ fontSize: 10, color: 'var(--text-muted)' }} />
</span>
</Tooltip>
),
@ -184,7 +184,7 @@ export default function ScreeningPanel() {
<Col xs={24} sm={8}>
<div className="card">
<div className="text-caption" style={{ marginBottom: 4 }}>筛选模式</div>
<div style={{ fontFamily: 'var(--font-text)', fontSize: 15, fontWeight: 500 }}>
<div style={{ fontFamily: 'var(--font-ui)', fontSize: 15, fontWeight: 500 }}>
{SCREEN_MODES.find(m => m.value === mode)?.label}
</div>
</div>
@ -198,7 +198,7 @@ export default function ScreeningPanel() {
<Col xs={24} sm={8}>
<div className="card">
<div className="text-caption" style={{ marginBottom: 4 }}>通过数量</div>
<div className="text-data" style={{ fontSize: 28, fontWeight: 600, color: 'var(--color-buy)' }}>{stats.passed}</div>
<div className="text-data" style={{ fontSize: 28, fontWeight: 600, color: 'var(--buy)' }}>{stats.passed}</div>
</div>
</Col>
</Row>