看板跌停梯队改为真实跌停数据展示
移除监控中心告警组件,新增 DownLadderMini 组件调用 limit-ladder API(direction=down)获取真实跌停梯队数据, 配色用 red/bear 色系,与涨停梯队布局一致。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,19 +2,16 @@ import { useState, useEffect, useRef, type ReactNode } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { Activity, ArrowDownRight, ArrowUpRight, BarChart3, BellRing, Database, Flame, Gauge, LineChart, Loader2, Play, RefreshCw, Sparkles, Target, Timer } from 'lucide-react'
|
import { Activity, ArrowDownRight, ArrowUpRight, BarChart3, Database, Flame, Gauge, LineChart, Loader2, Play, RefreshCw, Sparkles, Target, Timer } from 'lucide-react'
|
||||||
import { DatePicker } from '@/components/DatePicker'
|
import { DatePicker } from '@/components/DatePicker'
|
||||||
import { api, type MarketSnapshotRow, type OverviewDimensionRankItem, type OverviewMarket, type AlertEvent } from '@/lib/api'
|
import { api, type MarketSnapshotRow, type OverviewDimensionRankItem, type OverviewMarket } from '@/lib/api'
|
||||||
import { QK } from '@/lib/queryKeys'
|
import { QK } from '@/lib/queryKeys'
|
||||||
import { fmtBigNum, fmtPct } from '@/lib/format'
|
import { fmtBigNum } from '@/lib/format'
|
||||||
import { useDataStatus, useCapabilities, useSettings } from '@/lib/useSharedQueries'
|
import { useDataStatus, useCapabilities, useSettings } from '@/lib/useSharedQueries'
|
||||||
import { SealedBadge } from '@/components/SealedBadge'
|
import { SealedBadge } from '@/components/SealedBadge'
|
||||||
import { StockPreviewDialog } from '@/components/StockPreviewDialog'
|
|
||||||
import { SettingsModal } from '@/components/data/SettingsModal'
|
import { SettingsModal } from '@/components/data/SettingsModal'
|
||||||
import { STAGE_LABELS } from '@/components/data/ActiveJobCard'
|
import { STAGE_LABELS } from '@/components/data/ActiveJobCard'
|
||||||
import { cn } from '@/lib/cn'
|
|
||||||
import { cnSignal } from '@/lib/signals'
|
|
||||||
import { boardTag } from '@/components/stock-table/primitives'
|
|
||||||
|
|
||||||
function n(v: number | null | undefined) {
|
function n(v: number | null | undefined) {
|
||||||
return typeof v === 'number' && Number.isFinite(v) ? v : null
|
return typeof v === 'number' && Number.isFinite(v) ? v : null
|
||||||
@@ -79,140 +76,6 @@ function SectionTitle({ icon: Icon, title, hint }: { icon: typeof Activity; titl
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 看板监控中心小组件 — 显示前 10 条触发记录 + 更多按钮
|
|
||||||
const _SOURCE_BADGE: Record<string, string> = {
|
|
||||||
strategy: 'bg-amber-400/10 text-amber-400',
|
|
||||||
signal: 'bg-accent/10 text-accent',
|
|
||||||
price: 'bg-emerald-400/10 text-emerald-400',
|
|
||||||
market: 'bg-purple-500/10 text-purple-400',
|
|
||||||
}
|
|
||||||
const _SOURCE_LABEL: Record<string, string> = {
|
|
||||||
strategy: '策略', signal: '信号', price: '价格', market: '异动',
|
|
||||||
}
|
|
||||||
const _SEVERITY_BAR: Record<string, string> = {
|
|
||||||
info: 'bg-accent/40', warn: 'bg-warning', critical: 'bg-danger',
|
|
||||||
}
|
|
||||||
|
|
||||||
function MonitorWidget() {
|
|
||||||
const [previewEv, setPreviewEv] = useState<AlertEvent | null>(null)
|
|
||||||
const alerts = useQuery({
|
|
||||||
queryKey: ['alerts', ''],
|
|
||||||
queryFn: () => api.alertsList({ days: 7, limit: 10 }),
|
|
||||||
refetchInterval: 10000,
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
})
|
|
||||||
const events: AlertEvent[] = alerts.data?.alerts ?? []
|
|
||||||
|
|
||||||
if (events.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="mt-1 py-6 text-center text-[11px] text-muted">暂无触发记录</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mt-1 space-y-1.5">
|
|
||||||
{events
|
|
||||||
.filter((ev: AlertEvent) => !(ev.source === 'strategy' && !ev.symbol))
|
|
||||||
.map((ev, i) => {
|
|
||||||
const sev = _SEVERITY_BAR[ev.severity ?? 'info'] ?? _SEVERITY_BAR.info
|
|
||||||
const pct = ev.change_pct ?? 0
|
|
||||||
const isStrategy = ev.source === 'strategy'
|
|
||||||
const sm = isStrategy ? ev.message?.match(/策略「([^」]+)」/) : null
|
|
||||||
const sname = sm ? sm[1] : ''
|
|
||||||
const isNew = ev.type === 'new_entry'
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={`${ev.ts}-${i}`}
|
|
||||||
initial={{ opacity: 0, y: -8, scale: 0.98 }}
|
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
||||||
transition={{ duration: 0.3, delay: Math.min(i * 0.03, 0.3) }}
|
|
||||||
className="relative overflow-hidden rounded-md border border-border/40 bg-surface/60 pl-2.5 pr-2 py-1.5 hover:border-border hover:bg-surface transition-colors"
|
|
||||||
>
|
|
||||||
<div className={cn('absolute left-0 top-0 h-full w-0.5', sev)} />
|
|
||||||
{/* 第一行: 代码 + 名称 + 价格 + 涨跌幅 (点击代码/名称弹日K) */}
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<button
|
|
||||||
onClick={() => ev.symbol && setPreviewEv(ev)}
|
|
||||||
title={ev.symbol ? `查看 ${ev.symbol} 日K` : undefined}
|
|
||||||
className="inline-flex items-center gap-1 min-w-0 shrink-0 rounded hover:bg-elevated/60 transition-colors -mx-0.5 px-0.5 cursor-pointer"
|
|
||||||
>
|
|
||||||
<span className="font-mono text-[10px] font-medium text-foreground/80 hover:text-accent">{ev.symbol?.replace(/\.(SH|SZ|BJ)$/, '')}</span>
|
|
||||||
{ev.symbol && (() => {
|
|
||||||
const board = boardTag(ev.symbol)
|
|
||||||
return board && (
|
|
||||||
<span className={`inline-flex items-center justify-center h-3 w-3 rounded text-[7px] font-bold leading-none border ${board.color}`}>
|
|
||||||
{board.label}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
{ev.name && <span className="text-[10px] text-secondary truncate max-w-[5rem] hover:text-foreground">{ev.name}</span>}
|
|
||||||
</button>
|
|
||||||
<span className="flex-1" />
|
|
||||||
{ev.price != null && (
|
|
||||||
<span className="text-[10px] font-mono text-foreground/60 shrink-0">{fmtPrice(ev.price)}</span>
|
|
||||||
)}
|
|
||||||
{ev.change_pct != null && (
|
|
||||||
<span className={cn('text-[10px] font-mono font-medium shrink-0 w-12 text-right', pct >= 0 ? 'text-danger' : 'text-bear')}>
|
|
||||||
{fmtPct(pct)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* 第二行: 策略类型走新格式, 其他走旧格式 */}
|
|
||||||
{isStrategy ? (
|
|
||||||
<div className="mt-0.5 flex items-center gap-1.5">
|
|
||||||
<span className={cn('text-[9px] font-medium', isNew ? 'text-danger' : 'text-emerald-400')}>
|
|
||||||
{isNew ? '进入' : '移出'}
|
|
||||||
</span>
|
|
||||||
<span className="text-[9px] text-muted">策略</span>
|
|
||||||
<span className="text-[9px] font-medium text-amber-400">「{sname}」</span>
|
|
||||||
<span className="flex-1" />
|
|
||||||
<span className="text-[8px] text-muted/50 shrink-0 font-mono">
|
|
||||||
{ev.ts ? new Date(ev.ts).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="mt-0.5 flex items-center gap-1.5">
|
|
||||||
<span className={cn('shrink-0 rounded px-1 py-px text-[8px] font-medium', _SOURCE_BADGE[ev.source] ?? 'bg-elevated text-muted')}>
|
|
||||||
{_SOURCE_LABEL[ev.source] ?? ev.source}
|
|
||||||
</span>
|
|
||||||
{ev.message && (
|
|
||||||
<span className="text-[9px] text-muted truncate flex-1">{ev.message}</span>
|
|
||||||
)}
|
|
||||||
<span className="text-[8px] text-muted/50 shrink-0 font-mono">
|
|
||||||
{ev.ts ? new Date(ev.ts).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{ev.signals && ev.signals.length > 0 && (
|
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
|
||||||
{ev.signals.map((s, j) => (
|
|
||||||
<span key={j} className="rounded bg-accent/8 px-1 py-px text-[8px] text-accent/80">{cnSignal(s)}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<StockPreviewDialog
|
|
||||||
symbol={previewEv?.symbol ?? null}
|
|
||||||
name={previewEv?.name ?? undefined}
|
|
||||||
triggerInfo={previewEv ? {
|
|
||||||
price: previewEv.price ?? null,
|
|
||||||
changePct: previewEv.change_pct ?? null,
|
|
||||||
ts: previewEv.ts,
|
|
||||||
signals: previewEv.signals,
|
|
||||||
message: previewEv.message,
|
|
||||||
} : null}
|
|
||||||
onClose={() => setPreviewEv(null)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function KpiCell({ label, value, sub, tone = 'neutral' }: { label: ReactNode; value: ReactNode; sub?: string; tone?: 'bull' | 'bear' | 'accent' | 'neutral' }) {
|
function KpiCell({ label, value, sub, tone = 'neutral' }: { label: ReactNode; value: ReactNode; sub?: string; tone?: 'bull' | 'bear' | 'accent' | 'neutral' }) {
|
||||||
const isPlain = typeof value === 'string' || typeof value === 'number'
|
const isPlain = typeof value === 'string' || typeof value === 'number'
|
||||||
@@ -375,6 +238,30 @@ function LadderMini({ limit }: { limit: OverviewMarket['limit'] }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DownLadderMini() {
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['limit-ladder-down'],
|
||||||
|
queryFn: () => api.limitLadder(undefined, undefined, 'down'),
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
const tiers = data?.tiers?.filter(t => t.boards >= 2).slice(0, 6) ?? []
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{isLoading && <div className="rounded border border-dashed border-border py-5 text-center text-xs text-muted">加载中…</div>}
|
||||||
|
{!isLoading && tiers.length === 0 && <div className="rounded border border-dashed border-border py-5 text-center text-xs text-muted">暂无 2 板以上</div>}
|
||||||
|
{tiers.map(t => (
|
||||||
|
<div key={t.boards} className="grid grid-cols-[42px_1fr_auto] items-center gap-2 rounded bg-elevated/35 px-2 py-1.5">
|
||||||
|
<span className={`font-mono text-sm font-bold ${t.boards >= 5 ? 'text-bear' : t.boards >= 3 ? 'text-warning' : 'text-secondary'}`}>{t.boards}板</span>
|
||||||
|
<div className="h-1.5 overflow-hidden rounded-full bg-base">
|
||||||
|
<div className="h-full rounded-full bg-bear/70" style={{ width: `${Math.min(100, t.count * 12)}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-xs text-foreground">{t.count}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function MiniMetric({ label, value, cls = 'text-foreground' }: { label: string; value: string; cls?: string }) {
|
function MiniMetric({ label, value, cls = 'text-foreground' }: { label: string; value: string; cls?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded bg-elevated/45 px-2 py-1.5">
|
<div className="rounded bg-elevated/45 px-2 py-1.5">
|
||||||
@@ -728,17 +615,8 @@ export function Dashboard() {
|
|||||||
<LadderMini limit={data.limit} />
|
<LadderMini limit={data.limit} />
|
||||||
</section>
|
</section>
|
||||||
<section className="rounded-card border border-border bg-surface/80 p-3">
|
<section className="rounded-card border border-border bg-surface/80 p-3">
|
||||||
<div className="mb-2 flex items-center justify-between gap-2">
|
<SectionTitle icon={ArrowDownRight} title="跌停梯队" hint={<span className="text-bear">{data.limit.limit_down} 只</span>} />
|
||||||
<div className="flex items-center gap-1.5">
|
<DownLadderMini />
|
||||||
<BellRing className="h-3.5 w-3.5 text-accent" />
|
|
||||||
<h2 className="text-xs font-semibold text-foreground">跌停梯队</h2>
|
|
||||||
<span className="font-mono text-[10px] text-muted">封板强度</span>
|
|
||||||
</div>
|
|
||||||
<Link to="/monitor" className="inline-flex items-center justify-center h-5 w-5 rounded text-muted hover:text-accent hover:bg-accent/10 transition-colors" title="进入跌停梯队">
|
|
||||||
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<MonitorWidget />
|
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user