diff --git a/frontend/src/pages/Onboarding.tsx b/frontend/src/pages/Onboarding.tsx index d77b692..78c1445 100644 --- a/frontend/src/pages/Onboarding.tsx +++ b/frontend/src/pages/Onboarding.tsx @@ -1,14 +1,398 @@ -import { Sparkles } from 'lucide-react' -import { EmptyState } from '@/components/EmptyState' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { motion, AnimatePresence } from 'framer-motion' +import { + Eye, + EyeOff, + Loader2, + Save, + Check, + CheckCircle2, + AlertCircle, + ArrowRight, + ArrowLeft, + ExternalLink, + Copy, + Sparkles, + LineChart, + ScanSearch, + Flame, + Zap, +} from 'lucide-react' +import { api } from '@/lib/api' +import { useCapabilities, useSettings } from '@/lib/useSharedQueries' +import { QK } from '@/lib/queryKeys' +import { CAP_LABELS } from '@/lib/capability-labels' +import { Logo } from '@/components/Logo' + +// ===== 引导页:4 步向导 ===== +// 1. 欢迎 2. 输入 Key(可跳过) 3. 能力探测结果 4. 完成 → 写标记 → 进面板 + +const STEPS = ['欢迎', '配置 Key', '能力探测', '完成'] as const + +const HIGHLIGHTS = [ + { icon: LineChart, title: '看板与自选', desc: '实时行情、MA/MACD 指标、自定义自选列表' }, + { icon: ScanSearch, title: '策略选股', desc: '内置多套选股策略,一键扫描全市场命中' }, + { icon: Flame, title: '连板梯队', desc: '涨停板梯队、概念行业热度、市场情绪一览' }, + { icon: Zap, title: '回测验证', desc: '策略历史回测、因子分析,用数据说话' }, +] export function Onboarding() { + const navigate = useNavigate() + const qc = useQueryClient() + + const [step, setStep] = useState(0) + + // 完成向导 —— 写后端标记,使守卫放行 + const complete = useMutation({ + mutationFn: api.completeOnboarding, + onSuccess: () => { + qc.invalidateQueries({ queryKey: QK.settings }) + navigate('/', { replace: true }) + }, + onError: () => { + // 标记失败不应阻塞用户进入面板,仍放行 + navigate('/', { replace: true }) + }, + }) + + const finish = () => complete.mutate() + return ( -
- +
+ {/* 顶栏:logo + 进度指示 */} +
+
+ + TF-Stocks-Panel +
+
+ {STEPS.map((label, i) => ( +
+
+
+ ))} +
+
+ + {step + 1} / {STEPS.length} + +
+
+ + {/* 步骤内容 */} +
+
+ + + {step === 0 && setStep(1)} onSkip={finish} />} + {step === 1 && ( + setStep(2)} onSkip={() => setStep(2)} onBack={() => setStep(0)} /> + )} + {step === 2 && setStep(1)} />} + + +
+
+
+ ) +} + +// ===== Step 0: 欢迎 ===== + +function WelcomeStep({ onNext, onSkip }: { onNext: () => void; onSkip: () => void }) { + return ( +
+
+ +
+

欢迎使用 TF-Stocks-Panel

+

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

+ +
+ {HIGHLIGHTS.map((h) => ( +
+ +
{h.title}
+
{h.desc}
+
+ ))} +
+ +
+ + +
+
+ ) +} + +// ===== Step 1: 输入 TickFlow Key ===== + +function KeyStep({ onNext, onSkip, onBack }: { onNext: () => void; onSkip: () => void; onBack: () => void }) { + const qc = useQueryClient() + const settings = useSettings() + + const [keyInput, setKeyInput] = useState('') + const [revealing, setRevealing] = useState(false) + const [copiedCode, setCopiedCode] = useState(false) + const [saved, setSaved] = useState(false) + + const save = useMutation({ + mutationFn: () => api.saveTickflowKey(keyInput.trim()), + onSuccess: () => { + setSaved(true) + qc.invalidateQueries({ queryKey: QK.settings }) + qc.invalidateQueries({ queryKey: QK.capabilities }) + // 保存成功后自动进入下一步看探测结果 + setTimeout(() => onNext(), 600) + }, + }) + + const alreadyHasKey = settings.data?.has_tickflow_key + + return ( +
+

配置 TickFlow API Key

+

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

+ + {/* 注册引导 */} +
+ 还没有 Key?前往{' '} + + tickflow.org + + {' '} + 注册,或填写邀请码{' '} + + V3KDKGXPEA + + + ,即可免费领取扩展数据。 +
+ + {/* Key 已配置提示 */} + {alreadyHasKey && !save.isPending && ( +
+ + + 已检测到配置好的 Key({settings.data?.tickflow_api_key_masked})。 + 可直接下一步查看能力,或在下方粘贴新 Key 替换。 + +
+ )} + + {/* 输入 */} +
{ + e.preventDefault() + if (keyInput.trim()) save.mutate() + }} + className="mt-4 space-y-2" + > +
+ { + 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" + /> + +
+ + {/* 保存中提示 */} + {save.isPending && ( +
+ + 正在验证 Key 并探测能力,验证通过前请不要离开当前页面。 +
+ )} + + {save.isError && ( +
保存失败:{String((save.error as any).message)}
+ )} +
+ + {/* 底部操作 */} +
+ +
+ + +
+
+
+ ) +} + +// ===== Step 2: 能力探测结果 ===== + +function ResultStep({ onNext, onBack }: { onNext: () => void; onBack: () => void }) { + const settings = useSettings() + const caps = useCapabilities() + + const hasKey = settings.data?.has_tickflow_key + const capList = caps.data ? Object.entries(caps.data.capabilities) : [] + + return ( +
+

能力探测结果

+ + {hasKey ? ( + <> +

+ Key 已生效,以下是你当前可用的全部能力。后续可在 + 设置 → 账户 + 中重新检测或更换 Key。 +

+ +
+
+ 订阅档位 + + {caps.data?.label ?? settings.data?.tier_label ?? '—'} + +
+ + {caps.isLoading ? ( +
+ + 正在探测能力… +
+ ) : capList.length > 0 ? ( +
+ {capList.slice(0, 8).map(([cap]) => { + const meta = CAP_LABELS[cap] + return ( +
+ + {meta?.name ?? cap} +
+ ) + })} + {capList.length > 8 && ( +
…等共 {capList.length} 项
+ )} +
+ ) : ( +
暂未探测到能力
+ )} +
+ + ) : ( +
+
+ +
+
将以 Free 模式继续
+

+ 你可以立即试用基础行情与选股功能。需要扩展数据时,随时在 + 设置 → 账户 + 配置 Key。 +

+
+ )} + + {/* 底部操作 */} +
+ + +
) } diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 7d03fd4..e682ca8 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -18,12 +18,44 @@ import { Branding } from './pages/Branding' import { Settings } from './pages/Settings' import { Indices } from './pages/Indices' import { MinuteDataProbe } from './pages/MinuteDataProbe' +import { useSettings } from './lib/useSharedQueries' +import { Logo } from './components/Logo' + +// 首次使用守卫 —— 未完成向导则重定向到 /onboarding +// 只挂在根路由上;/onboarding 本身不被守卫,避免循环重定向。 +// settings 由 Layout 预取,守卫判定不产生额外请求。 +function OnboardingGuard({ children }: { children: React.ReactNode }) { + const settings = useSettings() + + // 加载中:显示极简全屏占位,避免闪屏 + if (settings.isLoading) { + return ( +
+
+ +
加载中…
+
+
+ ) + } + + // 查询出错或字段缺失时不拦截 —— 宁可放行,也不把用户卡在空白页 + if (settings.data && settings.data.onboarding_completed === false) { + return + } + + return <>{children} +} export const router = createBrowserRouter([ { path: '/onboarding', element: }, { path: '/', - element: , + element: ( + + + + ), children: [ { index: true, element: }, { path: 'overview', element: },