diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2ca47eb..67ec645 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -28,7 +28,7 @@ import { Pickaxe, FileText, Settings, - Activity, + DatabaseZap, Database, Loader2, LayoutDashboard, @@ -229,7 +229,7 @@ function TierBadge({ label, hasKey, providerName, isTickflow }: { label: string; className="pointer-events-none absolute inset-y-1.5 left-0 w-[2px] rounded-full bg-accent/50 transition-colors group-hover:bg-accent" style={base === 'expert' ? { background: 'linear-gradient(180deg, #60a5fa, #c084fc, #fbbf24)' } : undefined} /> - + {providerName || '数据源'} diff --git a/frontend/src/pages/Onboarding.tsx b/frontend/src/pages/Onboarding.tsx index a983622..3ba6cda 100644 --- a/frontend/src/pages/Onboarding.tsx +++ b/frontend/src/pages/Onboarding.tsx @@ -1,18 +1,13 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { motion, AnimatePresence } from 'framer-motion' import { - Eye, - EyeOff, Loader2, - Save, - Check, CheckCircle2, AlertCircle, ArrowRight, ArrowLeft, - ExternalLink, Sparkles, LineChart, ScanSearch, @@ -25,17 +20,20 @@ import { FileText, Landmark, Database, + Plus, + Puzzle, } from 'lucide-react' import { api } from '@/lib/api' -import { useCapabilities, useSettings } from '@/lib/useSharedQueries' +import { useCapabilities, usePreferences, useSettings } from '@/lib/useSharedQueries' import { QK } from '@/lib/queryKeys' import { CAP_LABELS } from '@/lib/capability-labels' import { Logo } from '@/components/Logo' +import { DataSourceEditor } from '@/pages/settings/DataSourceEditor' // ===== 引导页:5 步向导 ===== // 0. 声明 1. 欢迎 2. 输入 Key(可跳过) 3. 能力探测结果 4. 完成 → 写标记 → 进面板 -const STEPS = ['声明', '欢迎', '配置 Key', '能力探测', '完成'] as const +const STEPS = ['声明', '欢迎', '数据源', '能力探测', '完成'] as const const BRAND = '#8B5CF6' @@ -137,9 +135,9 @@ export function Onboarding() { - {/* 步骤内容 */} + {/* 步骤内容 (数据源步骤含编辑器, 加宽容器) */}
-
+
setStep(1)} />} {step === 1 && setStep(2)} onSkip={finish} />} {step === 2 && ( - setStep(3)} onSkip={() => setStep(3)} onBack={() => setStep(1)} /> + setStep(3)} onBack={() => setStep(1)} /> )} {step === 3 && setStep(4)} onBack={() => setStep(2)} />} {step === 4 && setStep(3)} pending={complete.isPending} />} @@ -185,23 +183,14 @@ function DisclaimerStep({ onNext }: { onNext: () => void }) {

- 本项目为个人开源项目, - - TickFlow - - - 官方项目。本项目独立维护,当前支持 TickFlow 数据服务。 + 本项目为个人开源项目,由个人独立维护,与任何商业数据服务 + 无官方关联。数据能力依赖第三方数据服务提供。

仅供学习研究使用,不构成任何投资建议。股市有风险,使用本项目产生的任何盈亏由使用者自行承担。

- 本项目基于 MIT 协议开源。使用本项目时,请遵守数据源(TickFlow 等)的服务条款;stock-sdk 等第三方接口插件存在版权与反爬风险,使用需自行评估合规责任。 + 本项目基于 MIT 协议开源。使用本项目时,请遵守所用数据源的服务条款;第三方接口插件存在版权与反爬风险,使用需自行评估合规责任。

@@ -237,7 +226,7 @@ function WelcomeStep({ onNext, onSkip }: { onNext: () => void; onSkip: () => voi

- 欢迎使用 Tick Stock Panel + 欢迎使用 TSP

一个本地化的 A 股量化分析面板 —— 行情、选股、回测、监控、财务一体化。 @@ -285,167 +274,220 @@ function WelcomeStep({ onNext, onSkip }: { onNext: () => void; onSkip: () => voi ) } -// ===== Step 2: 输入 TickFlow Key ===== +// ===== Step 2: 配置第三方数据源 (默认内置源; 可添加自有数据源) ===== -function KeyStep({ onNext, onSkip, onBack }: { onNext: () => void; onSkip: () => void; onBack: () => void }) { +/** datasets 标签的中文名 (与设置页数据集口径一致) */ +const DATASET_LABELS: Record = { + daily: '日K', + adj_factor: '除权', + realtime: '实时', + minute: '分钟K', + financial: '财务', +} + +function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () => void }) { const qc = useQueryClient() - const settings = useSettings() - - const [keyInput, setKeyInput] = useState('') - const [revealing, setRevealing] = useState(false) - const [saved, setSaved] = useState(false) - - const save = useMutation({ - mutationFn: () => api.saveTickflowKey(keyInput.trim()), - onSuccess: (data) => { - qc.invalidateQueries({ queryKey: QK.settings }) - qc.invalidateQueries({ queryKey: QK.capabilities }) - if (data.ok) { - // 仅当 key 有效(被存储)时才进入下一步看探测结果 - setSaved(true) - setTimeout(() => onNext(), 600) - } - // ok=false(key 无效):不进入下一步,错误提示由 save.error / save.data 渲染 - }, + const prefs = usePreferences() + const sources = useQuery({ + queryKey: QK.dataSources, + queryFn: api.dataSources, + staleTime: 60_000, }) - // 已配置 key —— 免费档或付费档都算(只要不是 None 档) - const alreadyHasKey = settings.data?.mode !== 'none' && settings.data?.mode !== undefined + // null = 跟随后端当前激活源 (首次使用即默认内置源) + const [picked, setPicked] = useState(null) + // 是否展开「添加自有数据源」编辑器 + const [adding, setAdding] = useState(false) + + const builtin = sources.data?.builtin ?? [] + const plugins = sources.data?.plugins ?? [] + const custom = sources.data?.custom ?? [] + const items = [ + ...builtin.map(s => ({ ...s, kind: 'builtin' as const })), + ...plugins.map(s => ({ ...s, kind: 'plugin' as const })), + ...custom.map(s => ({ ...s, kind: 'custom' as const })), + ] + const byName = new Map(items.map(s => [s.name, s])) + + const activeName = prefs.data?.daily_data_provider || 'tickflow' + const selected = picked ?? activeName + + // 切换数据源 —— 与设置页同一套偏好接口: + // 内置源 5 个数据集全量切换; 其他源仅切换其声明支持的数据集, 其余回落内置源 + const switchProvider = useMutation({ + mutationFn: (name: string) => { + if (name === 'tickflow') { + return api.updateDataProviders({ + daily_data_provider: 'tickflow', + adj_factor_provider: 'same_as_daily', + realtime_data_provider: 'tickflow', + minute_data_provider: 'tickflow', + financial_data_provider: 'tickflow', + }) + } + const supported = new Set(byName.get(name)?.datasets ?? []) + const pick = (dataset: string) => (supported.has(dataset) ? name : 'tickflow') + return api.updateDataProviders({ + daily_data_provider: pick('daily'), + adj_factor_provider: 'same_as_daily', + realtime_data_provider: pick('realtime'), + minute_data_provider: pick('minute'), + financial_data_provider: pick('financial'), + }) + }, + onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }), + }) + + const choose = (name: string, available: boolean) => { + if (!available || name === selected || switchProvider.isPending) return + setAdding(false) + setPicked(name) + switchProvider.mutate(name) + } return (

- +
-

配置 TickFlow API Key

+

配置数据源

- 本项目基于 TickFlow 这款稳定的数据源为基座进行开发,正在适配其他第三方数据源。 - 如果有任何建议或意见,欢迎发送邮件至{' '} - - 415333856@qq.com - - 。 + 所有数据源均为第三方服务,按需选择或添加自有接口;随时可在 + 设置 → 数据源 + 中调整。

- {/* 档位对比说明 —— None 档 vs Free 档 */} -
- {/* None 档 —— 不配置时默认 */} -
-
- None - 不配置(默认) -
-
    -
  • · 仅历史日K数据,无实时行情
  • -
  • · 数据有延迟,盘后约 1-2 小时更新当天
  • -
  • · 可用于策略回测、盘后分析
  • -
+ {/* 数据源卡片选择器 */} + {sources.isLoading ? ( +
+ + 正在加载数据源…
- {/* Free 档 —— 免费注册即可获取 */} -
-
- Free - 注册免费获取 - 推荐 -
-
    -
  • · 无需付费,注册即享
  • -
  • · 历史日K + 限定范围内的实时数据
  • -
  • · 可指定个股进行实时监控
  • -
-
-
+ ) : ( +
+ {items.map(item => { + const isSelected = selected === item.name + const unavailable = item.kind === 'plugin' && !item.available + const plugin = item.kind === 'plugin' ? plugins.find(p => p.name === item.name) : undefined + return ( + + ) + })} - {/* Key 已配置提示 */} - {alreadyHasKey && !save.isPending && ( -
- - - 已检测到配置好的 Key({settings.data?.tickflow_api_key_masked})。 - 可直接下一步查看能力,或在下方粘贴新 Key 替换。 - + {/* 添加自有数据源卡片 */} +
)} - {/* 获取 Key 的说明 —— 黄框卡片 */} -
- - - Key 可在{' '} - - tickflow.org - - - 获取。 - - 当前数据源基于 TickFlow 基座,其他第三方数据源正在开发适配中。 - - + {/* 插件化提示: 标识数据源体系已插件化, 并给出两条接入路径与文档指引 */} +
+ +
+ 数据源已插件化 + ,接入自有行情有两条路径:用 YAML 描述自有 HTTP 接口,放入 + data/data_sources/*.yaml + (也可用上方表单配置);或开发插件源,放入 + backend/app/plugins/ + 目录。接入方法与字段映射详见 + docs/custom-data-source.md + 与 + docs/plugin-development.md + 。 +
- {/* 输入 */} -
{ - 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.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" - /> - + {/* 切换反馈 */} + {switchProvider.isPending && ( +
+ + 正在切换数据源…
+ )} + {switchProvider.isError && ( +
+ + 数据源切换失败:{String((switchProvider.error as any)?.message ?? '')} +
+ )} - {/* 保存中提示 */} - {save.isPending && ( -
- - 正在验证 Key 并探测能力,验证通过前请不要离开当前页面。 -
+ {/* 添加自有数据源: 复用设置页编辑器 (命名/鉴权/数据集字段映射/测试/启用) */} + + {adding && ( + + setAdding(false)} + onSaved={() => { + qc.invalidateQueries({ queryKey: QK.dataSources }) + setAdding(false) + }} + activeName={activeName} + onActivate={name => switchProvider.mutate(name)} + /> + )} - - {save.isError && ( -
保存失败:{String((save.error as any).message)}
- )} - {/* 无效 key —— 探测失败(key 无效/乱填)未存储,提示用户 */} - {save.data && !save.data.ok && ( -
- - - {save.data.reason === 'invalid' - ? 'Key 无效或已过期,请检查后重试(未保存该 Key)。' - : save.data.error ?? '保存失败'} - -
- )} - +
{/* 底部操作 */}
@@ -456,29 +498,13 @@ function KeyStep({ onNext, onSkip, onBack }: { onNext: () => void; onSkip: () => 上一步 -
- - -
+
)