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}
/>