From 560a2d77359ec7fafca916c497f9b9050e33ceb2 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Sun, 30 Aug 2026 19:05:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(onboarding):=20API=20Key=20=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=B8=B8=E9=A9=BB=E5=B1=95=E5=BC=80,=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=8A=98=E5=8F=A0=E6=94=B6=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Onboarding.tsx | 235 +++++++++++++----------------- 1 file changed, 101 insertions(+), 134 deletions(-) diff --git a/frontend/src/pages/Onboarding.tsx b/frontend/src/pages/Onboarding.tsx index 8caa0f1..5fa884c 100644 --- a/frontend/src/pages/Onboarding.tsx +++ b/frontend/src/pages/Onboarding.tsx @@ -292,11 +292,10 @@ function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () => staleTime: 60_000, }) - // 内联 Key 配置目标 (tickflow = 档位 Key; fuyao = 实时/除权增强路由) - const [keyFor, setKeyFor] = useState(null) - const [keyInput, setKeyInput] = useState('') - const [keyError, setKeyError] = useState(null) - const [keySaved, setKeySaved] = useState(null) + // 两个常驻 Key 表单 (不收起): 输入/错误/已保存提示按数据源名分槽 + const [inputs, setInputs] = useState>({}) + const [errors, setErrors] = useState>({}) + const [savedMsg, setSavedMsg] = useState>({}) const plugins = sources.data?.plugins ?? [] const builtin = sources.data?.builtin ?? [] @@ -307,45 +306,41 @@ function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () => ...custom.map(s => ({ ...s, kind: 'custom' as const })), ] - const TICKFLOW_KEY_META = { - name: 'tickflow', display_name: 'TickFlow', api_key_env: 'TICKFLOW_API_KEY', - homepage: 'https://tickflow.org/auth/register?ref=V3KDKGXPEA', - } - const keyPlugin = keyFor - ? (plugins.find(p => p.name === keyFor) ?? (keyFor === 'tickflow' ? TICKFLOW_KEY_META : undefined)) - : undefined // 已配置态徽标: tickflow 看 settings.mode (free/api_key 均为已配 Key), fuyao 看插件 api_key_masked const tfConfigured = settings.data?.mode === 'free' || settings.data?.mode === 'api_key' const fuyaoConfigured = !!plugins.find(p => p.name === 'fuyao')?.api_key_masked // 内联 Key 配置 (先探后存): 验证通过才落盘 - const saveKey = useMutation({ - mutationFn: (name: string) => (name === 'tickflow' - ? api.saveTickflowKey(keyInput.trim()) - : api.savePluginKey(name, keyInput.trim())), - onSuccess: (data: any, name) => { + const saveKey = useMutation({ + mutationFn: ({ name, key }) => (name === 'tickflow' + ? api.saveTickflowKey(key) + : api.savePluginKey(name, key)), + onSuccess: (data: any, vars) => { qc.invalidateQueries({ queryKey: QK.dataSources }) qc.invalidateQueries({ queryKey: QK.capabilities }) qc.invalidateQueries({ queryKey: QK.capabilityMatrix }) + const setError = (msg: string | null) => setErrors(s => ({ ...s, [vars.name]: msg })) + const setSaved = (msg: string | null) => { + setSavedMsg(s => ({ ...s, [vars.name]: msg })) + if (msg) setTimeout(() => setSavedMsg(s => ({ ...s, [vars.name]: null })), 6000) + } if (data.ok) { - setKeyFor(null) - setKeyInput('') - setKeyError(null) - if (name === 'tickflow') { - setKeySaved(`TickFlow Key 已保存${data.tier_label ? `,当前档位:${data.tier_label}` : ''}`) - } else if (name === 'fuyao' && data.plugin_available) { + setInputs(s => ({ ...s, [vars.name]: '' })) + setError(null) + if (vars.name === 'tickflow') { + setSaved(`TickFlow Key 已保存${data.tier_label ? `,当前档位:${data.tier_label}` : ''}`) + } else if (vars.name === 'fuyao' && data.plugin_available) { // fuyao 定位为增强源: 仅实时行情 + 除权因子路由过去, 其余数据集保持 TickFlow routeFuyaoEnhanced.mutate() - setKeySaved('fuyao Key 已保存:实时行情与除权因子已切换到 fuyao,其余数据集保持 TickFlow') + setSaved('fuyao Key 已保存:实时行情与除权因子已切换到 fuyao,其余数据集保持 TickFlow') } else if (data.plugin_available) { - setKeySaved('Key 已保存,该数据源已就绪(可在设置中启用)') + setSaved('Key 已保存,该数据源已就绪') } - setTimeout(() => setKeySaved(null), 6000) } else { - setKeyError(data.error || (data.reason === 'invalid' ? 'Key 验证失败,请检查后重试' : '保存失败,请重试')) + setError(data.error || (data.reason === 'invalid' ? 'Key 验证失败,请检查后重试' : '保存失败,请重试')) } }, - onError: (e: Error) => setKeyError(`保存失败: ${e.message}`), + onError: (e: Error, vars) => setErrors(s => ({ ...s, [vars.name]: `保存失败: ${e.message}` })), }) // fuyao 增强路由: 只切实时行情 + 除权因子两个字段 (updateDataProviders 部分更新, 其余不动) @@ -357,6 +352,18 @@ function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () => onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }), }) + // 常驻展开的两个 Key 表单元数据 (卡片只读, Key 是向导里唯一可操作项) + const keyForms = [ + { + name: 'tickflow', display: 'TickFlow', env: 'TICKFLOW_API_KEY', configured: tfConfigured, autoFocus: true, + copy: '留空即免费 None 模式,可直接使用;填写 Key 后按订阅档位解锁实时 / 分钟 / 盘口 / 财务等更多数据集。仅存本地 (secrets.json),先验证后保存。', + }, + { + name: 'fuyao', display: 'fuyao', env: 'FUYAO_API_KEY', configured: fuyaoConfigured, autoFocus: false, + copy: '在官网申请 Key;仅存本地 (secrets.json),先验证后保存。保存成功后仅「实时行情」与「除权因子」切换到 fuyao,其余数据集保持 TickFlow。', + }, + ] + return (
@@ -381,27 +388,17 @@ function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () =>
{items.map(item => { const plugin = item.kind === 'plugin' ? plugins.find(p => p.name === item.name) : undefined - // 缺 Key 型插件 (如 fuyao) 卡片可点击展开 Key 表单; 其余纯展示 + // 缺 Key 型插件 (如 fuyao): 表单常驻下方, 卡片仅提示, 不可交互 const needsKey = item.kind === 'plugin' && !item.available && !!plugin?.api_key_env const unavailable = item.kind === 'plugin' && !item.available && !needsKey - const needsKeyProps = needsKey ? { - type: 'button' as const, - onClick: () => { - setKeyFor(keyFor === item.name ? null : item.name) - setKeyInput('') - setKeyError(null) - }, - title: '需配置 API Key,点击填写', - } : {} return (
@@ -422,7 +419,7 @@ function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () => {unavailable ? ( 需安装依赖,见 设置 → 数据源 ) : needsKey ? ( - 需配置 API Key,点击填写 + 需配置 API Key,见下方表单 ) : item.datasets.length > 0 ? ( item.datasets.slice(0, 4).map(ds => ( @@ -439,100 +436,70 @@ function DataSourceStep({ onNext, onBack }: { onNext: () => void; onBack: () =>
)} - {/* API Key 配置入口: TickFlow(档位解锁) + fuyao(实时/除权增强路由) */} - {!keyFor && !keySaved && ( -
- 配置 API Key: - - -
- )} - - {/* Key 保存成功横幅 (含路由变化说明) */} - {keySaved && !keyFor && ( -
- - {keySaved} -
- )} - - {/* 内联 Key 配置 (缺 Key 型插件如 fuyao / TickFlow 档位 Key): 先探后存 */} - {keyFor && keyPlugin && ( -
-
- - - 配置 {keyPlugin.display_name.replace(/(.*?)|\(.*?\)/g, '').trim()} API Key - - {keyPlugin.api_key_env} -
-

- {keyFor === 'tickflow' ? ( - <> - 留空即免费 None 模式,可直接使用;填写 Key 后按订阅档位解锁实时 / 分钟 / 盘口 / 财务等更多数据集。 - 仅存本地 (secrets.json),先验证后保存。 - - ) : keyFor === 'fuyao' ? ( - '在官网申请 Key;仅存本地 (secrets.json),先验证后保存。保存成功后仅「实时行情」与「除权因子」切换到 fuyao,其余数据集保持 TickFlow。' - ) : ( - '向数据源官方申请 Key;仅存本地 (secrets.json),先验证后保存。' + {/* API Key 配置 (常驻展开, 不收起): TickFlow 解锁档位 + fuyao 实时/除权增强路由 */} + {keyForms.map(f => { + const val = inputs[f.name] ?? '' + const err = errors[f.name] ?? null + const msg = savedMsg[f.name] ?? null + const pending = saveKey.isPending && saveKey.variables?.name === f.name + return ( +

+
+ + 配置 {f.display} API Key + {f.env} + {f.configured ? ( + + + 已配置 + + ) : f.name === 'tickflow' && ( + 可选 + )} +
+

{f.copy}

+
{ + e.preventDefault() + if (val.trim() && !saveKey.isPending) saveKey.mutate({ name: f.name, key: val.trim() }) + }} + > + { + setInputs(s => ({ ...s, [f.name]: e.target.value })) + setErrors(s => ({ ...s, [f.name]: null })) + }} + placeholder={`粘贴 ${f.env}`} + autoFocus={f.autoFocus} + className="h-8 flex-1 rounded-input border border-border bg-surface px-3 font-mono text-xs text-foreground placeholder:text-muted/60 focus:border-accent/60 focus:outline-none" + /> + +
+ {err && ( +

+ + {err} +

)} -

-
{ - e.preventDefault() - if (keyInput.trim() && !saveKey.isPending) saveKey.mutate(keyFor) - }} - > - { setKeyInput(e.target.value); setKeyError(null) }} - placeholder={`粘贴 ${keyPlugin.api_key_env}`} - autoFocus - className="h-8 flex-1 rounded-input border border-border bg-surface px-3 font-mono text-xs text-foreground placeholder:text-muted/60 focus:border-accent/60 focus:outline-none" - /> - - -
- {keyError && ( -

- - {keyError} -

- )} -
- )} + {msg && ( +

+ + {msg} +

+ )} +
+ ) + })} {/* 插件化提示: 自有数据源接入与切换在设置页, 向导保持极简 */}