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 + 进度指示 */}
+
+
+ {/* 步骤内容 */}
+
+
+
+
+ {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 替换。
+
+
+ )}
+
+ {/* 输入 */}
+
+
+ {/* 底部操作 */}
+
+
+
+
+
+
+
+
+ )
+}
+
+// ===== 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:
},