diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2a46515..820b62a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -81,7 +81,9 @@ function fmtIndexPct(v: number | null | undefined) { function indexPctClass(v: number | null | undefined) { if (v == null || Number.isNaN(Number(v))) return 'text-muted' - return Number(v) >= 0 ? 'text-bull' : 'text-bear' + const n = Number(v) + if (n === 0) return 'text-foreground' + return n > 0 ? 'text-bull' : 'text-bear' } /** 监控中心未读徽标 — 仅在非监控页且有未读时显示。 */ diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 69c921c..0b01506 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -9,7 +9,10 @@ import { QK } from '@/lib/queryKeys' import { fmtBigNum, fmtPct } from '@/lib/format' import { useDataStatus, useCapabilities } from '@/lib/useSharedQueries' import { SealedBadge } from '@/components/SealedBadge' +import { StockPreviewDialog } from '@/components/StockPreviewDialog' import { cn } from '@/lib/cn' +import { cnSignal } from '@/lib/signals' +import { boardTag } from '@/components/stock-table/primitives' function n(v: number | null | undefined) { return typeof v === 'number' && Number.isFinite(v) ? v : null @@ -89,6 +92,7 @@ const _SEVERITY_BAR: Record = { } function MonitorWidget() { + const [previewEv, setPreviewEv] = useState(null) const alerts = useQuery({ queryKey: ['alerts', ''], queryFn: () => api.alertsList({ days: 7, limit: 10 }), @@ -104,48 +108,86 @@ function MonitorWidget() { } return ( -
- {events.map((ev, i) => { - const sev = _SEVERITY_BAR[ev.severity ?? 'info'] ?? _SEVERITY_BAR.info - const pct = ev.change_pct ?? 0 - return ( - -
- {/* 第一行: 代码 + 名称 + 价格 + 涨跌幅 */} -
- {ev.symbol?.replace(/\.(SH|SZ|BJ)$/, '')} - {ev.name && {ev.name}} - {ev.price != null && ( - {fmtPrice(ev.price)} - )} - {ev.change_pct != null && ( - = 0 ? 'text-danger' : 'text-bear')}> - {pct >= 0 ? '+' : ''}{fmtPct(pct)} + <> +
+ {events.map((ev, i) => { + const sev = _SEVERITY_BAR[ev.severity ?? 'info'] ?? _SEVERITY_BAR.info + const pct = ev.change_pct ?? 0 + return ( + +
+ {/* 第一行: 代码 + 名称 + 价格 + 涨跌幅 (点击代码/名称弹日K) */} +
+ + + {ev.price != null && ( + {fmtPrice(ev.price)} + )} + {ev.change_pct != null && ( + = 0 ? 'text-danger' : 'text-bear')}> + {fmtPct(pct)} + + )} +
+ {/* 第二行: 分类标签 + 触发消息 + 时间 */} +
+ + {_SOURCE_LABEL[ev.source] ?? ev.source} + {ev.message && ( + {ev.message} + )} + + {ev.ts ? new Date(ev.ts).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''} + +
+ {/* 第三行: 命中信号 (买入/卖出触发器) */} + {ev.signals && ev.signals.length > 0 && ( +
+ {ev.signals.map((s, j) => ( + {cnSignal(s)} + ))} +
)} -
- {/* 第二行: 分类标签 + 触发消息 + 时间 */} -
- - {_SOURCE_LABEL[ev.source] ?? ev.source} - - {ev.message && ( - {ev.message} - )} - - {ev.ts ? new Date(ev.ts).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''} - -
-
- ) - })} -
+ + ) + })} +
+ + setPreviewEv(null)} + /> + ) } diff --git a/frontend/src/pages/Onboarding.tsx b/frontend/src/pages/Onboarding.tsx index 91e79a9..3eb7558 100644 --- a/frontend/src/pages/Onboarding.tsx +++ b/frontend/src/pages/Onboarding.tsx @@ -19,6 +19,9 @@ import { ScanSearch, Flame, Zap, + Radar, + ShieldCheck, + BellRing, } from 'lucide-react' import { api } from '@/lib/api' import { useCapabilities, useSettings } from '@/lib/useSharedQueries' @@ -27,15 +30,19 @@ import { CAP_LABELS } from '@/lib/capability-labels' import { Logo } from '@/components/Logo' // ===== 引导页:4 步向导 ===== -// 1. 欢迎 2. 输入 Key(可跳过) 3. 能力探测结果 4. 完成 → 写标记 → 进面板 +// 0. 欢迎 1. 输入 Key(可跳过) 2. 能力探测结果 3. 完成 → 写标记 → 进面板 const STEPS = ['欢迎', '配置 Key', '能力探测', '完成'] as const +const BRAND = '#8B5CF6' + const HIGHLIGHTS = [ - { icon: LineChart, title: '看板与自选', desc: '实时行情、MA/MACD 指标、自定义自选列表' }, - { icon: ScanSearch, title: '策略选股', desc: '内置多套选股策略,一键扫描全市场命中' }, - { icon: Flame, title: '连板梯队', desc: '涨停板梯队、概念行业热度、市场情绪一览' }, - { icon: Zap, title: '回测验证', desc: '策略历史回测、因子分析,用数据说话' }, + { icon: LineChart, title: '看板与自选', desc: '实时行情、MA/MACD 指标、自定义自选列表', tint: 'text-accent' }, + { icon: ScanSearch, title: '策略选股', desc: '内置多套选股策略,一键扫描全市场命中', tint: 'text-bull' }, + { icon: Flame, title: '连板梯队', desc: '涨停板梯队、概念行业热度、市场情绪一览', tint: 'text-warning' }, + { icon: Radar, title: '实时监控', desc: '自定义条件 / 策略监控,触发即推送告警', tint: 'text-bear' }, + { icon: ShieldCheck, title: '回测验证', desc: '策略历史回测、因子分析,用数据说话', tint: 'text-accent' }, + { icon: BellRing, title: '本地优先', desc: '数据本地存储,隐私可控,断网仍可查阅', tint: 'text-bull' }, ] export function Onboarding() { @@ -47,7 +54,12 @@ export function Onboarding() { // 完成向导 —— 写后端标记,使守卫放行 const complete = useMutation({ mutationFn: api.completeOnboarding, - onSuccess: () => { + onSuccess: (data) => { + // 用接口返回值同步更新缓存,确保跳转时守卫立即看到 onboarding_completed: true + // (避免 invalidate 后台重取未返回时, 守卫用旧缓存 false 误重定向回引导页) + qc.setQueryData(QK.settings, (old: any) => + old ? { ...old, onboarding_completed: data.onboarding_completed } : old, + ) qc.invalidateQueries({ queryKey: QK.settings }) navigate('/', { replace: true }) }, @@ -60,34 +72,68 @@ export function Onboarding() { const finish = () => complete.mutate() return ( -
+
+ {/* 背景光晕 —— 品牌 + 主色渐变 */} +
+
+
+ {/* 极淡网格底纹 */} +
+
+ {/* 顶栏:logo + 进度指示 */} -
-
- - TickFlow Stock Panel +
+
+ + TickFlow Stock Panel
+ {/* 步骤进度条 —— 胶囊式 */}
{STEPS.map((label, i) => (
-
0 &&
} +
))}
- + {step + 1} / {STEPS.length}
{/* 步骤内容 */} -
-
+
+
setStep(2)} onSkip={() => setStep(2)} onBack={() => setStep(0)} /> )} - {step === 2 && setStep(1)} />} + {step === 2 && setStep(3)} onBack={() => setStep(1)} />} + {step === 3 && setStep(2)} pending={complete.isPending} />}
@@ -114,36 +161,54 @@ export function Onboarding() { function WelcomeStep({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) { return (
-
- -
-

欢迎使用 TickFlow Stock Panel

+ {/* 品牌 badge */} + + + + +

+ 欢迎使用 TickFlow Stock Panel +

- 一个本地化的 A 股量化分析面板 —— 行情、选股、回测、财务一体化。 + 一个本地化的 A 股量化分析面板 —— 行情、选股、回测、监控、财务一体化。 花一分钟配置,即可开始使用。

-
- {HIGHLIGHTS.map((h) => ( -
- + {/* 6 个特性卡片 */} +
+ {HIGHLIGHTS.map((h, i) => ( + +
{h.title}
{h.desc}
-
+ ))}
@@ -178,20 +243,25 @@ function KeyStep({ onNext, onSkip, onBack }: { onNext: () => void; onSkip: () => return (
-

配置 TickFlow API Key

-

+

+
+ +
+

配置 TickFlow API Key

+
+

Key 决定你能使用的数据范围。没有 Key 也能以 Free 模式试用基础功能; 配置后可解锁概念行业、财务数据等扩展能力。

{/* 注册引导 */} -
+
还没有 Key?前往{' '} tickflow.org @@ -245,12 +315,12 @@ function KeyStep({ onNext, onSkip, onBack }: { onNext: () => void; onSkip: () => setKeyInput(e.target.value) if (saved) setSaved(false) }} - className="w-full px-3 py-2 pr-9 rounded-input bg-base border border-border text-sm font-mono focus:outline-none focus:border-accent transition-colors" + className="w-full px-3 py-2.5 pr-9 rounded-input bg-base border border-border text-sm font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-all" />
) } + +// ===== Step 3: 完成 ===== + +function FinishStep({ onNext, onBack, pending }: { onNext: () => void; onBack: () => void; pending: boolean }) { + const tips = [ + { icon: ScanSearch, text: '在「选股」页用内置策略一键扫描全市场' }, + { icon: BellRing, text: '在「监控」页设置条件或策略告警,盘中实时推送' }, + { icon: ShieldCheck, text: '在「回测」页用历史数据验证策略表现' }, + ] + + return ( +
+ +
+ + {/* 光晕脉冲 */} + +
+
+ +

一切就绪!

+

+ 配置已完成。下面几个入口帮你快速上手,有任何问题随时在 + 设置 里调整。 +

+ + {/* 快速上手提示 */} +
+ {tips.map((t, i) => ( + +
+ +
+ {t.text} +
+ ))} +
+ + {/* 底部操作 */} +
+ + +
+
+ ) +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 354ec94..cf778c9 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -27,8 +27,9 @@ import { Logo } from './components/Logo' function OnboardingGuard({ children }: { children: React.ReactNode }) { const settings = useSettings() - // 加载中:显示极简全屏占位,避免闪屏 - if (settings.isLoading) { + // 加载中 或 正在后台重新请求 → 显示占位,避免用旧缓存误判 + // (根治 invalidate 触发重取未返回时, 守卫用旧 onboarding_completed=false 误重定向的竞态) + if (settings.isLoading || settings.isFetching) { return (