From b4dae4bca1c09a8fc2c8bff670e65376f334436e Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Tue, 21 Jul 2026 21:39:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=82=A1=E7=A5=A8=E4=B8=8EET?= =?UTF-8?q?F=E5=88=87=E6=8D=A2=E8=AF=AF=E6=B8=85=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/api.ts | 4 ++-- frontend/src/pages/Screener.tsx | 42 +++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index caf62bf..b335575 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1402,9 +1402,9 @@ export const api = { : '/api/watchlist/enriched', ), - screenerStrategies: async (assetType: 'stock' | 'etf' = 'stock') => { + screenerStrategies: async (assetType?: 'stock' | 'etf') => { const data = await request<{ strategies: StrategyDetail[]; load_errors?: StrategyLoadError[] }>( - `/api/strategies?asset_type=${assetType}&timeframe=1d`, + `/api/strategies?${assetType ? `asset_type=${assetType}&` : ''}timeframe=1d`, ) return { presets: data.strategies, load_errors: data.load_errors } }, diff --git a/frontend/src/pages/Screener.tsx b/frontend/src/pages/Screener.tsx index 82cdba9..6422176 100644 --- a/frontend/src/pages/Screener.tsx +++ b/frontend/src/pages/Screener.tsx @@ -120,8 +120,8 @@ export function Screener() { const screenerAutoRun = prefs?.screener_auto_run ?? true const strategies = useQuery({ - queryKey: QK.screenerStrategies(assetType), - queryFn: () => api.screenerStrategies(assetType), + queryKey: QK.screenerStrategies('all'), + queryFn: () => api.screenerStrategies(), }) // 卡片首屏只读取轻量摘要;明细在点击策略或“全部”时按需加载。 @@ -154,25 +154,37 @@ export function Screener() { if (latest) setAsOf(latest) }, [dataStatus.data?.enriched?.latest_date]) + const strategyPresets = useMemo( + () => (strategies.data?.presets ?? []).filter(s => s.asset_types.includes(assetType)), + [strategies.data, assetType], + ) + // 策略 ID → 名称映射 const strategyIdToName = useMemo(() => { const map: Record = {} - for (const p of strategies.data?.presets ?? []) { + for (const p of strategyPresets) { map[p.id] = p.name } return map - }, [strategies.data]) + }, [strategyPresets]) // 策略 ID → 完整对象映射(避免每张卡片 find 遍历) const strategyMap = useMemo(() => { const map = new Map() - for (const p of strategies.data?.presets ?? []) { + for (const p of strategyPresets) { map.set(p.id, p) } return map - }, [strategies.data]) + }, [strategyPresets]) - const availableStrategyIds = useMemo(() => new Set((strategies.data?.presets ?? []).map(s => s.id)), [strategies.data]) + const allStrategyIds = useMemo( + () => new Set((strategies.data?.presets ?? []).map(s => s.id)), + [strategies.data], + ) + const availableStrategyIds = useMemo( + () => new Set(strategyPresets.map(s => s.id)), + [strategyPresets], + ) const visiblePool = useMemo(() => pool.filter(id => availableStrategyIds.has(id)), [pool, availableStrategyIds]) // 策略列表加载后,自动清除池中失效的自定义策略(如本地开发残留的、 @@ -183,9 +195,9 @@ export function Screener() { useEffect(() => { if (strategies.isError) return // 拉取失败: 不 prune if (!strategies.isSuccess) return // 加载中: 不 prune - if (availableStrategyIds.size === 0) return // 空列表: 不 prune - prune(availableStrategyIds) - }, [availableStrategyIds, prune, strategies.isError, strategies.isSuccess]) + if (allStrategyIds.size === 0) return // 空列表: 不 prune + prune(allStrategyIds) + }, [allStrategyIds, prune, strategies.isError, strategies.isSuccess]) // 策略文件加载失败时提示用户(避免"策略静默消失"被误判为正常) const loadErrors = strategies.data?.load_errors ?? [] @@ -400,7 +412,7 @@ export function Screener() { useEffect(() => { // ETF 模式无股票盘后缓存/ runAll, 单策略走实时单跑, 不触发 runAll if (assetType !== 'stock') return - if (!asOf || !strategies.data?.presets?.length || !summaryQuery.isSuccess || runAll.isPending || visiblePool.length === 0) return + if (!asOf || strategyPresets.length === 0 || !summaryQuery.isSuccess || runAll.isPending || visiblePool.length === 0) return const runKey = `${asOf}|${visiblePool.join(',')}` if (runAllDateRef.current === runKey) return // 缓存已覆盖当前策略池 → 秒加载, 不触发 runAll @@ -412,7 +424,7 @@ export function Screener() { if (!screenerAutoRun) return runAllDateRef.current = runKey runAll.mutate({ date: asOf, strategyIds: missingStrategyIds }) - }, [asOf, strategies.data, summaryQuery.isSuccess, visiblePool, cacheCoversPool, missingStrategyIds, screenerAutoRun, assetType, runAll.isPending]) + }, [asOf, strategyPresets.length, summaryQuery.isSuccess, visiblePool, cacheCoversPool, missingStrategyIds, screenerAutoRun, assetType, runAll.isPending]) const run = useMutation({ mutationFn: ({ id, date }: { id: string; date: string }) => @@ -621,7 +633,7 @@ export function Screener() { 策略池 - {visiblePool.length}/{strategies.data?.presets?.length ?? 0} + {visiblePool.length}/{strategyPresets.length} {/* 创建策略 */} @@ -943,9 +955,9 @@ export function Screener() { open={showBuilder} onClose={() => setShowBuilder(false)} mode={builderMode} - existingStrategyIds={availableStrategyIds} + existingStrategyIds={allStrategyIds} onSavedId={async id => { - const data = await qc.fetchQuery({ queryKey: QK.screenerStrategies('stock'), queryFn: () => api.screenerStrategies('stock'), staleTime: 0 }) + const data = await qc.fetchQuery({ queryKey: QK.screenerStrategies('all'), queryFn: () => api.screenerStrategies(), staleTime: 0 }) if (!data.presets.some(s => s.id === id)) { throw new Error(`策略 ${id} 已保存但未加载,请检查策略代码`) }