mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
feat(screener): 策略列标签折叠 — 表头全表开关 + 行级展开
- 默认收起: 每行显示前 N 个策略 + 计数 (跟随列设置"显示前N个", 未配置默认 3) - 表头图标切换全表展开/收起, 收起时清除行级展开状态, 状态持久化 - 排列方向始终跟随列设置 tagLayout (展开不再强制横向)
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
* score、signals、candle、ext 列。其余纯数据列(价格/指标/财务…)交给共享原语。
|
* score、signals、candle、ext 列。其余纯数据列(价格/指标/财务…)交给共享原语。
|
||||||
*/
|
*/
|
||||||
import { useState, type CSSProperties, type ReactNode } from 'react'
|
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 type { KlineRow, MinuteKlineRow } from '@/lib/api'
|
||||||
import { fmtPrice, formatExtNumber } from '@/lib/format'
|
import { fmtPrice, formatExtNumber } from '@/lib/format'
|
||||||
import type { ColumnConfig } from '@/lib/screener-columns'
|
import type { ColumnConfig } from '@/lib/screener-columns'
|
||||||
@@ -50,12 +50,17 @@ interface ScreenerTableProps {
|
|||||||
onRefreshIntraday?: () => void
|
onRefreshIntraday?: () => void
|
||||||
/** 分时数据正在刷新中 (按钮 loading 态) */
|
/** 分时数据正在刷新中 (按钮 loading 态) */
|
||||||
intradayRefreshing?: boolean
|
intradayRefreshing?: boolean
|
||||||
|
/** 策略列标签全表展开状态 (false=默认收起: 每行仅显示首个策略+计数) */
|
||||||
|
strategyTagsExpanded?: boolean
|
||||||
|
/** 表头策略列图标: 切换全表展开/收起 */
|
||||||
|
onToggleStrategyTags?: () => void
|
||||||
/** 表头排序(受控,由 Screener.tsx 传入) */
|
/** 表头排序(受控,由 Screener.tsx 传入) */
|
||||||
sort?: SortState | null
|
sort?: SortState | null
|
||||||
onSortToggle?: (colId: string) => void
|
onSortToggle?: (colId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渲染标签数组(含 maxTags 折叠/展开、横竖排列)。策略列与 ext 列共用。 */
|
/** 渲染标签数组(含 maxTags 折叠/展开、横竖排列)。策略列与 ext 列共用。
|
||||||
|
* maxTagsOverride: 调用方直接指定折叠上限 (策略列用: 全局展开=0 不折叠, 收起=1 只显首个)。 */
|
||||||
function renderTagList(
|
function renderTagList(
|
||||||
tags: string[],
|
tags: string[],
|
||||||
col: ColumnConfig,
|
col: ColumnConfig,
|
||||||
@@ -63,11 +68,12 @@ function renderTagList(
|
|||||||
onToggle: () => void,
|
onToggle: () => void,
|
||||||
tagClassName: string,
|
tagClassName: string,
|
||||||
onTagClick?: (tag: string) => void,
|
onTagClick?: (tag: string) => void,
|
||||||
|
maxTagsOverride?: number,
|
||||||
): ReactNode {
|
): ReactNode {
|
||||||
if (tags.length === 0) return <span className="text-muted">—</span>
|
if (tags.length === 0) return <span className="text-muted">—</span>
|
||||||
|
|
||||||
const cfg = col.extDisplay
|
const cfg = col.extDisplay
|
||||||
const maxTags = cfg?.maxTags ?? 0
|
const maxTags = maxTagsOverride ?? cfg?.maxTags ?? 0
|
||||||
const showAll = maxTags <= 0 || expanded || tags.length <= maxTags
|
const showAll = maxTags <= 0 || expanded || tags.length <= maxTags
|
||||||
const sliced = showAll ? tags : tags.slice(0, maxTags)
|
const sliced = showAll ? tags : tags.slice(0, maxTags)
|
||||||
const hiddenIndices = maxTags > 0 ? cfg?.hiddenIndices : undefined
|
const hiddenIndices = maxTags > 0 ? cfg?.hiddenIndices : undefined
|
||||||
@@ -75,7 +81,8 @@ function renderTagList(
|
|||||||
? sliced.filter((_, i) => !hiddenIndices.includes(i))
|
? sliced.filter((_, i) => !hiddenIndices.includes(i))
|
||||||
: sliced
|
: sliced
|
||||||
const hiddenCount = tags.length - visibleTags.length
|
const hiddenCount = tags.length - visibleTags.length
|
||||||
const isVertical = cfg?.tagLayout === 'vertical' && !expanded
|
// 排列方向始终跟随列设置: 竖向时收起/展开都竖排, 展开不再强制横向
|
||||||
|
const isVertical = cfg?.tagLayout === 'vertical'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isVertical ? 'flex flex-col items-start gap-0.5' : 'flex flex-wrap gap-0.5'}>
|
<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,
|
dailyKChartVisible = true, onToggleDailyKChart,
|
||||||
minuteData = {}, intradayChartVisible = true, onToggleIntradayChart,
|
minuteData = {}, intradayChartVisible = true, onToggleIntradayChart,
|
||||||
intradayAutoRefresh = false, onRefreshIntraday, intradayRefreshing = false,
|
intradayAutoRefresh = false, onRefreshIntraday, intradayRefreshing = false,
|
||||||
|
strategyTagsExpanded = false, onToggleStrategyTags,
|
||||||
sort, onSortToggle,
|
sort, onSortToggle,
|
||||||
}: ScreenerTableProps) {
|
}: ScreenerTableProps) {
|
||||||
const [expandedCells, setExpandedCells] = useState<Set<string>>(new Set())
|
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 => {
|
const renderCell = (r: any, col: ColumnConfig): ReactNode => {
|
||||||
// ext 列
|
// ext 列
|
||||||
if (col.source.type === 'ext') {
|
if (col.source.type === 'ext') {
|
||||||
@@ -278,9 +299,13 @@ export function ScreenerTable({
|
|||||||
const tags = strats.map(sid => strategyIdToName[sid] ?? sid)
|
const tags = strats.map(sid => strategyIdToName[sid] ?? sid)
|
||||||
const cellKey = `${r.symbol}::${col.id}`
|
const cellKey = `${r.symbol}::${col.id}`
|
||||||
const expanded = expandedCells.has(cellKey)
|
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 (
|
return (
|
||||||
<td key={col.id} className="px-3 py-2">
|
<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>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -435,6 +460,27 @@ export function ScreenerTable({
|
|||||||
</span>
|
</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
|
return undefined
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export const storage = {
|
|||||||
/** 策略结果列表分时图显示状态 */
|
/** 策略结果列表分时图显示状态 */
|
||||||
screenerIntraday: kv<boolean>('screener_showIntraday'),
|
screenerIntraday: kv<boolean>('screener_showIntraday'),
|
||||||
|
|
||||||
|
/** 策略结果列表"策略"列标签展开状态 (false=默认收起: 每行首个+计数, 行内可单独展开) */
|
||||||
|
screenerStrategyTags: kv<boolean>('screener_strategyTagsExpanded'),
|
||||||
|
|
||||||
/** 自选列表板块筛选 */
|
/** 自选列表板块筛选 */
|
||||||
watchlistBoardFilter: kv<string[]>('watchlist_boardFilter'),
|
watchlistBoardFilter: kv<string[]>('watchlist_boardFilter'),
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ export function Screener() {
|
|||||||
return next
|
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 [intradayCapDismissed, setIntradayCapDismissed] = useState(false)
|
||||||
const [showAll, setShowAll] = useState(false)
|
const [showAll, setShowAll] = useState(false)
|
||||||
@@ -979,6 +988,8 @@ export function Screener() {
|
|||||||
intradayAutoRefresh={intradayRefreshEnabled && realtimeRunning}
|
intradayAutoRefresh={intradayRefreshEnabled && realtimeRunning}
|
||||||
onRefreshIntraday={() => minuteBatch.refetch()}
|
onRefreshIntraday={() => minuteBatch.refetch()}
|
||||||
intradayRefreshing={minuteBatch.isFetching}
|
intradayRefreshing={minuteBatch.isFetching}
|
||||||
|
strategyTagsExpanded={strategyTagsExpanded}
|
||||||
|
onToggleStrategyTags={toggleStrategyTags}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSortToggle={toggle}
|
onSortToggle={toggle}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user