mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
fix(onboarding): 修复引导页二次进入的问题,优化看板监控中心
引导页竞态修复 (走完向导后又被弹回第一页): - Onboarding.tsx: complete mutation onSuccess 用 setQueryData 同步写入 onboarding_completed 缓存, 避免跳转时守卫读到旧值 false 误重定向 - router.tsx: 守卫条件由 isLoading 升级为 isLoading || isFetching, settings 后台重取期间显示加载态, 兜底根治同类竞态 引导页内容丰富 + 样式美化: - 补全 Step 3 完成页 (原 STEPS 有 4 项但只渲染 3 步), 4 步闭环 - 欢迎页特性卡片扩展到 6 个 (新增实时监控/本地优先) - 背景加品牌紫+主色蓝双光晕渐变与网格底纹 - 进度条改胶囊式, 卡片毛玻璃 hover 上浮, 完成页 check 光晕脉冲 看板监控中心优化 (Dashboard.tsx MonitorWidget): - 修复涨跌幅双重加号 (++10.01%) — fmtPct 已含正号, 删除手动拼接 - 新增命中信号 chips 展示 (cnSignal 转中文, 与监控中心一致) - 代码/名称改为可点击, 弹出 StockPreviewDialog 日K预览 - 代码后加创业板/科创板/北交所板块标识 (boardTag) 附带: Layout.tsx 指数卡片 0.0% 显示白色 (原被 >=0 归入红色)
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
/** 监控中心未读徽标 — 仅在非监控页且有未读时显示。 */
|
||||
|
||||
@@ -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<string, string> = {
|
||||
}
|
||||
|
||||
function MonitorWidget() {
|
||||
const [previewEv, setPreviewEv] = useState<AlertEvent | null>(null)
|
||||
const alerts = useQuery({
|
||||
queryKey: ['alerts', ''],
|
||||
queryFn: () => api.alertsList({ days: 7, limit: 10 }),
|
||||
@@ -104,48 +108,86 @@ function MonitorWidget() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-1 space-y-1.5">
|
||||
{events.map((ev, i) => {
|
||||
const sev = _SEVERITY_BAR[ev.severity ?? 'info'] ?? _SEVERITY_BAR.info
|
||||
const pct = ev.change_pct ?? 0
|
||||
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)} />
|
||||
{/* 第一行: 代码 + 名称 + 价格 + 涨跌幅 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] font-medium text-foreground/80 shrink-0">{ev.symbol?.replace(/\.(SH|SZ|BJ)$/, '')}</span>
|
||||
{ev.name && <span className="text-[10px] text-secondary truncate flex-1">{ev.name}</span>}
|
||||
{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')}>
|
||||
{pct >= 0 ? '+' : ''}{fmtPct(pct)}
|
||||
<>
|
||||
<div className="mt-1 space-y-1.5">
|
||||
{events.map((ev, i) => {
|
||||
const sev = _SEVERITY_BAR[ev.severity ?? 'info'] ?? _SEVERITY_BAR.info
|
||||
const pct = ev.change_pct ?? 0
|
||||
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>
|
||||
{/* 第二行: 分类标签 + 触发消息 + 时间 */}
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-base flex flex-col">
|
||||
<div className="relative min-h-screen bg-base overflow-hidden flex flex-col">
|
||||
{/* 背景光晕 —— 品牌 + 主色渐变 */}
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
className="absolute -top-40 -left-40 h-[28rem] w-[28rem] rounded-full blur-[120px] opacity-20"
|
||||
style={{ background: `radial-gradient(circle, ${BRAND}, transparent 70%)` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-40 -right-32 h-[26rem] w-[26rem] rounded-full blur-[120px] opacity-15"
|
||||
style={{ background: 'radial-gradient(circle, hsl(var(--accent)), transparent 70%)' }}
|
||||
/>
|
||||
{/* 极淡网格底纹 */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.025]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(hsl(var(--fg-primary)) 1px, transparent 1px), linear-gradient(90deg, hsl(var(--fg-primary)) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 顶栏:logo + 进度指示 */}
|
||||
<header className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
<Logo size={22} />
|
||||
<span className="text-sm font-medium">TickFlow Stock Panel</span>
|
||||
<header className="relative z-10 flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-2.5 text-foreground">
|
||||
<Logo
|
||||
size={24}
|
||||
className="shrink-0"
|
||||
style={{ color: BRAND, filter: `drop-shadow(0 0 8px ${BRAND}55)` }}
|
||||
/>
|
||||
<span className="text-sm font-semibold tracking-tight">TickFlow Stock Panel</span>
|
||||
</div>
|
||||
{/* 步骤进度条 —— 胶囊式 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{STEPS.map((label, i) => (
|
||||
<div key={label} className="flex items-center gap-1.5">
|
||||
<div
|
||||
className={`h-1.5 rounded-full transition-all duration-300 ${
|
||||
i === step ? 'w-6 bg-accent' : i < step ? 'w-1.5 bg-accent' : 'w-1.5 bg-border'
|
||||
}`}
|
||||
{i > 0 && <div className="h-px w-3 bg-border" />}
|
||||
<motion.div
|
||||
animate={{
|
||||
width: i === step ? 64 : 24,
|
||||
backgroundColor: i === step
|
||||
? 'hsl(var(--accent))'
|
||||
: i < step
|
||||
? 'hsl(var(--accent) / 0.6)'
|
||||
: 'hsl(var(--border))',
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="h-1.5 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-[88px] text-right">
|
||||
<span className="text-xs text-muted">
|
||||
<span className="text-xs text-muted tabular">
|
||||
{step + 1} / {STEPS.length}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
<main className="flex-1 flex items-center justify-center px-6 py-10">
|
||||
<div className="w-full max-w-lg">
|
||||
<main className="relative z-10 flex-1 flex items-center justify-center px-6 py-10">
|
||||
<div className="w-full max-w-xl">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={step}
|
||||
@@ -100,7 +146,8 @@ export function Onboarding() {
|
||||
{step === 1 && (
|
||||
<KeyStep onNext={() => setStep(2)} onSkip={() => setStep(2)} onBack={() => setStep(0)} />
|
||||
)}
|
||||
{step === 2 && <ResultStep onNext={finish} onBack={() => setStep(1)} />}
|
||||
{step === 2 && <ResultStep onNext={() => setStep(3)} onBack={() => setStep(1)} />}
|
||||
{step === 3 && <FinishStep onNext={finish} onBack={() => setStep(2)} pending={complete.isPending} />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
@@ -114,36 +161,54 @@ export function Onboarding() {
|
||||
function WelcomeStep({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mx-auto w-fit rounded-2xl bg-accent/10 p-4">
|
||||
<Sparkles className="h-8 w-8 text-accent" />
|
||||
</div>
|
||||
<h1 className="mt-6 text-2xl font-bold text-foreground">欢迎使用 TickFlow Stock Panel</h1>
|
||||
{/* 品牌 badge */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.85, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mx-auto w-fit rounded-2xl p-4 border border-border"
|
||||
style={{ background: `linear-gradient(135deg, ${BRAND}22, transparent)` }}
|
||||
>
|
||||
<Sparkles className="h-8 w-8" style={{ color: BRAND }} />
|
||||
</motion.div>
|
||||
|
||||
<h1 className="mt-6 text-3xl font-bold text-foreground tracking-tight">
|
||||
欢迎使用 TickFlow Stock Panel
|
||||
</h1>
|
||||
<p className="mt-3 text-sm text-secondary leading-relaxed max-w-md mx-auto">
|
||||
一个本地化的 A 股量化分析面板 —— 行情、选股、回测、财务一体化。
|
||||
一个本地化的 A 股量化分析面板 —— 行情、选股、回测、监控、财务一体化。
|
||||
花一分钟配置,即可开始使用。
|
||||
</p>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 gap-3 text-left">
|
||||
{HIGHLIGHTS.map((h) => (
|
||||
<div key={h.title} className="rounded-card border border-border bg-surface p-4">
|
||||
<h.icon className="h-5 w-5 text-accent" />
|
||||
{/* 6 个特性卡片 */}
|
||||
<div className="mt-8 grid grid-cols-2 sm:grid-cols-3 gap-3 text-left">
|
||||
{HIGHLIGHTS.map((h, i) => (
|
||||
<motion.div
|
||||
key={h.title}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.05 * i + 0.1 }}
|
||||
whileHover={{ y: -2 }}
|
||||
className="group rounded-card border border-border bg-surface/80 backdrop-blur-sm p-3.5 transition-colors hover:border-accent/30"
|
||||
>
|
||||
<h.icon className={`h-5 w-5 ${h.tint} transition-transform group-hover:scale-110`} />
|
||||
<div className="mt-2 text-sm font-medium text-foreground">{h.title}</div>
|
||||
<div className="mt-1 text-xs text-muted leading-relaxed">{h.desc}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={onNext}
|
||||
className="inline-flex items-center gap-2 px-5 h-10 rounded-xl bg-accent text-white text-sm font-semibold hover:bg-accent/90 transition-colors"
|
||||
className="inline-flex items-center gap-2 px-6 h-11 rounded-xl bg-accent text-white text-sm font-semibold shadow-lg shadow-accent/20 hover:bg-accent/90 hover:shadow-accent/30 transition-all"
|
||||
>
|
||||
开始配置
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onSkip}
|
||||
className="px-4 h-10 rounded-xl text-sm text-secondary hover:text-foreground transition-colors"
|
||||
className="px-4 h-11 rounded-xl text-sm text-secondary hover:text-foreground hover:bg-elevated transition-colors"
|
||||
>
|
||||
稍后再说
|
||||
</button>
|
||||
@@ -178,20 +243,25 @@ function KeyStep({ onNext, onSkip, onBack }: { onNext: () => void; onSkip: () =>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground">配置 TickFlow API Key</h2>
|
||||
<p className="mt-2 text-sm text-secondary leading-relaxed">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-accent/10 p-2">
|
||||
<ShieldCheck className="h-4 w-4 text-accent" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-foreground">配置 TickFlow API Key</h2>
|
||||
</div>
|
||||
<p className="mt-2.5 text-sm text-secondary leading-relaxed">
|
||||
Key 决定你能使用的数据范围。没有 Key 也能以 <span className="font-medium text-foreground">Free</span> 模式试用基础功能;
|
||||
配置后可解锁概念行业、财务数据等扩展能力。
|
||||
</p>
|
||||
|
||||
{/* 注册引导 */}
|
||||
<div className="mt-5 rounded-card border border-border bg-surface p-4 text-xs text-secondary leading-relaxed">
|
||||
<div className="mt-5 rounded-card border border-border bg-surface/80 backdrop-blur-sm p-4 text-xs text-secondary leading-relaxed">
|
||||
还没有 Key?前往{' '}
|
||||
<a
|
||||
href="https://tickflow.org/auth/register?ref=V3KDKGXPEA"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent hover:underline inline-flex items-baseline gap-0.5"
|
||||
className="text-accent hover:underline inline-flex items-baseline gap-0.5 font-medium"
|
||||
>
|
||||
tickflow.org
|
||||
<ExternalLink className="h-3 w-3 self-center" />
|
||||
@@ -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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRevealing((v) => !v)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted hover:text-foreground transition-colors"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted hover:text-foreground transition-colors"
|
||||
tabIndex={-1}
|
||||
aria-label={revealing ? '隐藏' : '显示'}
|
||||
>
|
||||
@@ -319,17 +389,22 @@ function ResultStep({ onNext, onBack }: { onNext: () => void; onBack: () => void
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-foreground">能力探测结果</h2>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-accent/10 p-2">
|
||||
<ScanSearch className="h-4 w-4 text-accent" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-foreground">能力探测结果</h2>
|
||||
</div>
|
||||
|
||||
{hasKey ? (
|
||||
<>
|
||||
<p className="mt-2 text-sm text-secondary leading-relaxed">
|
||||
<p className="mt-2.5 text-sm text-secondary leading-relaxed">
|
||||
Key 已生效,以下是你当前可用的全部能力。后续可在
|
||||
<span className="text-foreground"> 设置 → 账户 </span>
|
||||
<span className="text-foreground font-medium"> 设置 → 账户 </span>
|
||||
中重新检测或更换 Key。
|
||||
</p>
|
||||
|
||||
<div className="mt-5 rounded-card border border-border bg-surface p-5">
|
||||
<div className="mt-5 rounded-card border border-border bg-surface/80 backdrop-blur-sm p-5">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<span className="text-[10px] uppercase tracking-widest text-muted">订阅档位</span>
|
||||
<span className="font-mono text-2xl font-bold tracking-tight text-foreground">
|
||||
@@ -363,14 +438,14 @@ function ResultStep({ onNext, onBack }: { onNext: () => void; onBack: () => void
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-5 rounded-card border border-border bg-surface p-6 text-center">
|
||||
<div className="mt-5 rounded-card border border-border bg-surface/80 backdrop-blur-sm p-6 text-center">
|
||||
<div className="mx-auto w-fit rounded-xl bg-elevated p-3">
|
||||
<Zap className="h-6 w-6 text-warning" />
|
||||
</div>
|
||||
<div className="mt-3 text-sm font-medium text-foreground">将以 Free 模式继续</div>
|
||||
<p className="mt-2 text-xs text-muted leading-relaxed max-w-sm mx-auto">
|
||||
你可以立即试用基础行情与选股功能。需要扩展数据时,随时在
|
||||
<span className="text-foreground"> 设置 → 账户 </span>
|
||||
<span className="text-foreground font-medium"> 设置 → 账户 </span>
|
||||
配置 Key。
|
||||
</p>
|
||||
</div>
|
||||
@@ -389,10 +464,87 @@ function ResultStep({ onNext, onBack }: { onNext: () => void; onBack: () => void
|
||||
onClick={onNext}
|
||||
className="inline-flex items-center gap-2 px-5 h-9 rounded-xl bg-accent text-white text-sm font-semibold hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
进入面板
|
||||
下一步
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== 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 (
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0.85, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mx-auto w-fit"
|
||||
>
|
||||
<div
|
||||
className="relative rounded-2xl p-5 border border-border"
|
||||
style={{ background: `linear-gradient(135deg, ${BRAND}22, transparent)` }}
|
||||
>
|
||||
<CheckCircle2 className="h-12 w-12 text-bear" />
|
||||
{/* 光晕脉冲 */}
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.4], opacity: [0.4, 0] }}
|
||||
transition={{ duration: 1.8, repeat: Infinity, ease: 'easeOut' }}
|
||||
className="absolute inset-5 rounded-full bg-bear/30"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<h1 className="mt-6 text-2xl font-bold text-foreground">一切就绪!</h1>
|
||||
<p className="mt-2.5 text-sm text-secondary leading-relaxed max-w-md mx-auto">
|
||||
配置已完成。下面几个入口帮你快速上手,有任何问题随时在
|
||||
<span className="text-foreground font-medium"> 设置 </span>里调整。
|
||||
</p>
|
||||
|
||||
{/* 快速上手提示 */}
|
||||
<div className="mt-6 space-y-2 text-left">
|
||||
{tips.map((t, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 * i + 0.2 }}
|
||||
className="flex items-center gap-3 rounded-card border border-border bg-surface/80 backdrop-blur-sm px-3.5 py-2.5"
|
||||
>
|
||||
<div className="rounded-lg bg-accent/10 p-1.5 shrink-0">
|
||||
<t.icon className="h-3.5 w-3.5 text-accent" />
|
||||
</div>
|
||||
<span className="text-xs text-secondary">{t.text}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="inline-flex items-center gap-1.5 px-3 h-10 rounded-btn text-sm text-secondary hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
上一步
|
||||
</button>
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={pending}
|
||||
className="inline-flex items-center gap-2 px-6 h-10 rounded-xl bg-accent text-white text-sm font-semibold shadow-lg shadow-accent/20 hover:bg-accent/90 hover:shadow-accent/30 disabled:opacity-60 transition-all"
|
||||
>
|
||||
{pending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
|
||||
{pending ? '正在进入…' : '进入面板'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-base grid place-items-center">
|
||||
<div className="flex flex-col items-center gap-3 text-muted">
|
||||
|
||||
Reference in New Issue
Block a user