mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
feat(screener): 策略列标签折叠 — 表头全表开关 + 行级展开
- 默认收起: 每行显示前 N 个策略 + 计数 (跟随列设置"显示前N个", 未配置默认 3) - 表头图标切换全表展开/收起, 收起时清除行级展开状态, 状态持久化 - 排列方向始终跟随列设置 tagLayout (展开不再强制横向)
This commit is contained in:
@@ -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 <span className="text-muted">—</span>
|
||||
|
||||
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 (
|
||||
<div className={isVertical ? 'flex flex-col items-start gap-0.5' : 'flex flex-wrap gap-0.5'}>
|
||||
@@ -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<Set<string>>(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 (
|
||||
<td key={col.id} className="px-3 py-2">
|
||||
{renderTagList(tags, col, expanded, () => toggleExpand(cellKey), STRATEGY_TAG_CLS)}
|
||||
{renderTagList(tags, col, expanded, () => toggleExpand(cellKey), STRATEGY_TAG_CLS, undefined, maxTags)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
@@ -435,6 +460,27 @@ export function ScreenerTable({
|
||||
</span>
|
||||
)
|
||||
}
|
||||
// 策略列标签展开/收起开关 (命中多策略时行会很高, 默认收起只显首个+计数)
|
||||
if (key === 'strategies' && onToggleStrategyTags) {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center gap-1.5">
|
||||
<span>{col.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => { event.stopPropagation(); handleToggleStrategyTags() }}
|
||||
className={`inline-flex items-center justify-center w-5 h-5 rounded transition-colors ${
|
||||
strategyTagsExpanded
|
||||
? 'text-accent bg-accent/10 hover:bg-accent/20'
|
||||
: 'text-muted hover:text-foreground hover:bg-elevated'
|
||||
}`}
|
||||
title={strategyTagsExpanded ? '收起策略标签(每行仅显示前几个)' : '展开全部策略标签'}
|
||||
aria-label={strategyTagsExpanded ? '收起策略标签' : '展开全部策略标签'}
|
||||
>
|
||||
{strategyTagsExpanded ? <ListTree className="h-3.5 w-3.5" /> : <ListCollapse className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -62,6 +62,9 @@ export const storage = {
|
||||
/** 策略结果列表分时图显示状态 */
|
||||
screenerIntraday: kv<boolean>('screener_showIntraday'),
|
||||
|
||||
/** 策略结果列表"策略"列标签展开状态 (false=默认收起: 每行首个+计数, 行内可单独展开) */
|
||||
screenerStrategyTags: kv<boolean>('screener_strategyTagsExpanded'),
|
||||
|
||||
/** 自选列表板块筛选 */
|
||||
watchlistBoardFilter: kv<string[]>('watchlist_boardFilter'),
|
||||
|
||||
|
||||
@@ -77,6 +77,15 @@ export function Screener() {
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
// 策略列标签全表展开/收起(命中多策略时行会很高;默认收起, 每行可单独展开;持久化)
|
||||
const [strategyTagsExpanded, setStrategyTagsExpanded] = useState<boolean>(() => 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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user