From bdad0fefb53cfdfd8174385a69c2c85614a0db74 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Mon, 31 Aug 2026 22:30:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(screener):=20=E7=AD=96=E7=95=A5=E5=88=97?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=8A=98=E5=8F=A0=20=E2=80=94=20=E8=A1=A8?= =?UTF-8?q?=E5=A4=B4=E5=85=A8=E8=A1=A8=E5=BC=80=E5=85=B3=20+=20=E8=A1=8C?= =?UTF-8?q?=E7=BA=A7=E5=B1=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 默认收起: 每行显示前 N 个策略 + 计数 (跟随列设置"显示前N个", 未配置默认 3) - 表头图标切换全表展开/收起, 收起时清除行级展开状态, 状态持久化 - 排列方向始终跟随列设置 tagLayout (展开不再强制横向) --- .../src/components/screener/ScreenerTable.tsx | 56 +++++++++++++++++-- frontend/src/lib/storage.ts | 3 + frontend/src/pages/Screener.tsx | 11 ++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/screener/ScreenerTable.tsx b/frontend/src/components/screener/ScreenerTable.tsx index 8963586..09439c7 100644 --- a/frontend/src/components/screener/ScreenerTable.tsx +++ b/frontend/src/components/screener/ScreenerTable.tsx @@ -6,7 +6,7 @@ * score、signals、candle、ext 列。其余纯数据列(价格/指标/财务…)交给共享原语。 */ import { useState, type CSSProperties, type ReactNode } from 'react' -import { Check, Plus, Eye, EyeOff, RefreshCw } from 'lucide-react' +import { Check, Plus, Eye, EyeOff, RefreshCw, ListCollapse, ListTree } from 'lucide-react' import type { KlineRow, MinuteKlineRow } from '@/lib/api' import { fmtPrice, formatExtNumber } from '@/lib/format' import type { ColumnConfig } from '@/lib/screener-columns' @@ -50,12 +50,17 @@ interface ScreenerTableProps { onRefreshIntraday?: () => void /** 分时数据正在刷新中 (按钮 loading 态) */ intradayRefreshing?: boolean + /** 策略列标签全表展开状态 (false=默认收起: 每行仅显示首个策略+计数) */ + strategyTagsExpanded?: boolean + /** 表头策略列图标: 切换全表展开/收起 */ + onToggleStrategyTags?: () => void /** 表头排序(受控,由 Screener.tsx 传入) */ sort?: SortState | null onSortToggle?: (colId: string) => void } -/** 渲染标签数组(含 maxTags 折叠/展开、横竖排列)。策略列与 ext 列共用。 */ +/** 渲染标签数组(含 maxTags 折叠/展开、横竖排列)。策略列与 ext 列共用。 + * maxTagsOverride: 调用方直接指定折叠上限 (策略列用: 全局展开=0 不折叠, 收起=1 只显首个)。 */ function renderTagList( tags: string[], col: ColumnConfig, @@ -63,11 +68,12 @@ function renderTagList( onToggle: () => void, tagClassName: string, onTagClick?: (tag: string) => void, + maxTagsOverride?: number, ): ReactNode { if (tags.length === 0) return const cfg = col.extDisplay - const maxTags = cfg?.maxTags ?? 0 + const maxTags = maxTagsOverride ?? cfg?.maxTags ?? 0 const showAll = maxTags <= 0 || expanded || tags.length <= maxTags const sliced = showAll ? tags : tags.slice(0, maxTags) const hiddenIndices = maxTags > 0 ? cfg?.hiddenIndices : undefined @@ -75,7 +81,8 @@ function renderTagList( ? sliced.filter((_, i) => !hiddenIndices.includes(i)) : sliced const hiddenCount = tags.length - visibleTags.length - const isVertical = cfg?.tagLayout === 'vertical' && !expanded + // 排列方向始终跟随列设置: 竖向时收起/展开都竖排, 展开不再强制横向 + const isVertical = cfg?.tagLayout === 'vertical' return (
@@ -153,6 +160,7 @@ export function ScreenerTable({ dailyKChartVisible = true, onToggleDailyKChart, minuteData = {}, intradayChartVisible = true, onToggleIntradayChart, intradayAutoRefresh = false, onRefreshIntraday, intradayRefreshing = false, + strategyTagsExpanded = false, onToggleStrategyTags, sort, onSortToggle, }: ScreenerTableProps) { const [expandedCells, setExpandedCells] = useState>(new Set()) @@ -181,6 +189,19 @@ export function ScreenerTable({ }) } + // 策略列全表切换: 收起时清除该列的行级展开状态, 保证"收起"立即对全表生效 + const strategiesColId = columns.find(c => c.source.type === 'builtin' && c.source.key === 'strategies')?.id + const handleToggleStrategyTags = () => { + if (strategyTagsExpanded && strategiesColId) { + const suffix = `::${strategiesColId}` + setExpandedCells(prev => { + const next = new Set([...prev].filter(k => !k.endsWith(suffix))) + return next.size === prev.size ? prev : next + }) + } + onToggleStrategyTags?.() + } + const renderCell = (r: any, col: ColumnConfig): ReactNode => { // ext 列 if (col.source.type === 'ext') { @@ -278,9 +299,13 @@ export function ScreenerTable({ const tags = strats.map(sid => strategyIdToName[sid] ?? sid) const cellKey = `${r.symbol}::${col.id}` const expanded = expandedCells.has(cellKey) + // 收起时每行显示前N个 + "+N" 计数 (跟随列设置"显示前N个", 未配置默认 3), + // 点击计数/收起按钮可单独展开/收起本行; 全局展开: maxTags=0 全部显示不折叠 + const cfgMaxTags = col.extDisplay?.maxTags ?? 0 + const maxTags = strategyTagsExpanded ? 0 : (cfgMaxTags > 0 ? cfgMaxTags : 3) return ( - {renderTagList(tags, col, expanded, () => toggleExpand(cellKey), STRATEGY_TAG_CLS)} + {renderTagList(tags, col, expanded, () => toggleExpand(cellKey), STRATEGY_TAG_CLS, undefined, maxTags)} ) } @@ -435,6 +460,27 @@ export function ScreenerTable({ ) } + // 策略列标签展开/收起开关 (命中多策略时行会很高, 默认收起只显首个+计数) + if (key === 'strategies' && onToggleStrategyTags) { + return ( + + {col.label} + + + ) + } return undefined }} /> diff --git a/frontend/src/lib/storage.ts b/frontend/src/lib/storage.ts index 9ef231d..4b5a76b 100644 --- a/frontend/src/lib/storage.ts +++ b/frontend/src/lib/storage.ts @@ -62,6 +62,9 @@ export const storage = { /** 策略结果列表分时图显示状态 */ screenerIntraday: kv('screener_showIntraday'), + /** 策略结果列表"策略"列标签展开状态 (false=默认收起: 每行首个+计数, 行内可单独展开) */ + screenerStrategyTags: kv('screener_strategyTagsExpanded'), + /** 自选列表板块筛选 */ watchlistBoardFilter: kv('watchlist_boardFilter'), diff --git a/frontend/src/pages/Screener.tsx b/frontend/src/pages/Screener.tsx index e2b05e5..4fa37a1 100644 --- a/frontend/src/pages/Screener.tsx +++ b/frontend/src/pages/Screener.tsx @@ -77,6 +77,15 @@ export function Screener() { return next }) }, []) + // 策略列标签全表展开/收起(命中多策略时行会很高;默认收起, 每行可单独展开;持久化) + const [strategyTagsExpanded, setStrategyTagsExpanded] = useState(() => storage.screenerStrategyTags.get(false)) + const toggleStrategyTags = useCallback(() => { + setStrategyTagsExpanded(v => { + const next = !v + storage.screenerStrategyTags.set(next) + return next + }) + }, []) // 截断提示可关闭 (仅本次会话, 不持久化) const [intradayCapDismissed, setIntradayCapDismissed] = useState(false) const [showAll, setShowAll] = useState(false) @@ -979,6 +988,8 @@ export function Screener() { intradayAutoRefresh={intradayRefreshEnabled && realtimeRunning} onRefreshIntraday={() => minuteBatch.refetch()} intradayRefreshing={minuteBatch.isFetching} + strategyTagsExpanded={strategyTagsExpanded} + onToggleStrategyTags={toggleStrategyTags} sort={sort} onSortToggle={toggle} />