mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
perf(info-bar): 信息条指标弹窗打开/滑动/操作卡顿优化
ListColumnCustomizer: 新增 disableBackdropBlur prop (信息条场景传 true, 避免对背后 K线 Canvas 做 backdrop-blur 逐帧 GPU 合成 — 打开/滑动卡顿主因); columns/onChange 用 ref 镜像, 8 个 useCallback 改空依赖 (callback 引用稳定, 不再随 columns 变化重建导致 SortableActiveCol memo 失效); 4 个配置面板 prop 从 ReactNode 改为 () => ReactNode 惰性渲染 (未展开不构造); extTableLabelMap 包 useMemo。StockInfoBar: 信息条场景传 disableBackdropBlur。选股/自选页保持 backdrop-blur 不变 (背后是 DOM 表格, 开销可忽略)。
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
* - 下半区「内置列」:按业务分组折叠
|
||||
* - 底部「扩展数据列」:复用 ext_data schema,按需添加字段
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react'
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
DndContext, closestCenter, KeyboardSensor, PointerSensor,
|
||||
@@ -39,6 +39,13 @@ interface ListColumnCustomizerProps {
|
||||
showExtColumns?: boolean
|
||||
/** 是否显示「单独显示」勾选项(默认 false;仅信息条场景启用,让某列独占一行)。 */
|
||||
showStandaloneToggle?: boolean
|
||||
/**
|
||||
* 是否禁用遮罩背景模糊 (backdrop-blur)。默认 false。
|
||||
* 信息条场景 (宿主含 Canvas K线/分时图) 传 true:blur 会对 Canvas 做逐帧 GPU 合成,
|
||||
* 是抽屉打开/滑动卡顿的主因;改纯半透明遮罩后合成层锐减。
|
||||
* 选股/自选页背后是 DOM 表格,blur 开销可忽略,保持默认。
|
||||
*/
|
||||
disableBackdropBlur?: boolean
|
||||
}
|
||||
|
||||
/** 判断扩展数据字段类型是否为数字(int/float/double/number/decimal 等)。
|
||||
@@ -60,10 +67,10 @@ function SortableActiveCol({ col, onRemove, onConfig, configOpen, extTableLabel,
|
||||
onConfig: (id: string | null) => void
|
||||
configOpen: boolean
|
||||
extTableLabel: string
|
||||
extConfig: React.ReactNode
|
||||
candleConfig: React.ReactNode
|
||||
intradayConfig: React.ReactNode
|
||||
strategiesConfig: React.ReactNode
|
||||
extConfig: () => React.ReactNode
|
||||
candleConfig: () => React.ReactNode
|
||||
intradayConfig: () => React.ReactNode
|
||||
strategiesConfig: () => React.ReactNode
|
||||
showStandaloneToggle?: boolean
|
||||
onToggleStandalone?: (id: string) => void
|
||||
}) {
|
||||
@@ -132,7 +139,7 @@ function SortableActiveCol({ col, onRemove, onConfig, configOpen, extTableLabel,
|
||||
<EyeOff className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
{hasConfig && configOpen && (isExt ? extConfig : isCandle ? candlePanel : isIntraday ? intradayPanel : strategiesConfig)}
|
||||
{hasConfig && configOpen && (isExt ? extConfig() : isCandle ? candlePanel() : isIntraday ? intradayPanel() : strategiesConfig())}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -149,6 +156,7 @@ export function ListColumnCustomizer({
|
||||
extFieldFilter,
|
||||
showExtColumns = true,
|
||||
showStandaloneToggle = false,
|
||||
disableBackdropBlur = false,
|
||||
}: ListColumnCustomizerProps) {
|
||||
const extSchema = useQuery({
|
||||
queryKey: QK.extDataSchemaAll,
|
||||
@@ -158,6 +166,13 @@ export function ListColumnCustomizer({
|
||||
})
|
||||
const backdrop = useDialogBackdrop(onClose)
|
||||
|
||||
// columns/onChange 用 ref 镜像,让下方 8 个 useCallback 空依赖,
|
||||
// 避免 columns 每次变更都重建 callback 导致 SortableActiveCol 等 memo 失效。
|
||||
const columnsRef = useRef(columns)
|
||||
columnsRef.current = columns
|
||||
const onChangeRef = useRef(onChange)
|
||||
onChangeRef.current = onChange
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set())
|
||||
const [configOpenId, setConfigOpenId] = useState<string | null>(null)
|
||||
@@ -175,16 +190,18 @@ export function ListColumnCustomizer({
|
||||
const activeCols = useMemo(() => columns.filter(c => !c.pinned && c.visible), [columns])
|
||||
|
||||
const toggleVisible = useCallback((colId: string) => {
|
||||
onChange(columns.map(c =>
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c =>
|
||||
c.id === colId && !c.pinned ? { ...c, visible: !c.visible } : c
|
||||
))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const toggleStandalone = useCallback((colId: string) => {
|
||||
onChange(columns.map(c =>
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c =>
|
||||
c.id === colId ? { ...c, standalone: !c.standalone } : c
|
||||
))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
@@ -194,19 +211,21 @@ export function ListColumnCustomizer({
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) return
|
||||
const reorderCols = columns.filter(c => !c.pinned)
|
||||
const pinnedCols = columns.filter(c => c.pinned)
|
||||
const cols = columnsRef.current
|
||||
const reorderCols = cols.filter(c => !c.pinned)
|
||||
const pinnedCols = cols.filter(c => c.pinned)
|
||||
const ids = reorderCols.map(c => c.id)
|
||||
const oldIdx = ids.indexOf(active.id as string)
|
||||
const newIdx = ids.indexOf(over.id as string)
|
||||
if (oldIdx < 0 || newIdx < 0) return
|
||||
const reordered = arrayMove(reorderCols, oldIdx, newIdx)
|
||||
onChange([...pinnedCols, ...reordered])
|
||||
}, [columns, onChange])
|
||||
onChangeRef.current([...pinnedCols, ...reordered])
|
||||
}, [])
|
||||
|
||||
const addExtColumn = useCallback((configId: string, fieldName: string, fieldLabel?: string, fieldType?: string) => {
|
||||
const cols = columnsRef.current
|
||||
const colId = `ext:${configId}:${fieldName}`
|
||||
if (columns.some(c => c.id === colId)) {
|
||||
if (cols.some(c => c.id === colId)) {
|
||||
toggleVisible(colId)
|
||||
return
|
||||
}
|
||||
@@ -217,64 +236,71 @@ export function ListColumnCustomizer({
|
||||
visible: true,
|
||||
align: extColumnAlign,
|
||||
}
|
||||
const actionIdx = columns.findIndex(c => c.id === 'builtin:action')
|
||||
const actionIdx = cols.findIndex(c => c.id === 'builtin:action')
|
||||
if (actionIdx >= 0) {
|
||||
const next = [...columns]
|
||||
const next = [...cols]
|
||||
next.splice(actionIdx, 0, newCol)
|
||||
onChange(next)
|
||||
onChangeRef.current(next)
|
||||
} else {
|
||||
onChange([...columns, newCol])
|
||||
onChangeRef.current([...cols, newCol])
|
||||
}
|
||||
}, [columns, extColumnAlign, onChange, toggleVisible])
|
||||
}, [extColumnAlign, toggleVisible])
|
||||
|
||||
const hideColumn = useCallback((colId: string) => {
|
||||
onChange(columns.map(c => c.id === colId ? { ...c, visible: false } : c))
|
||||
}, [columns, onChange])
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => c.id === colId ? { ...c, visible: false } : c))
|
||||
}, [])
|
||||
|
||||
const updateExtDisplay = useCallback((colId: string, patch: Partial<ExtColumnDisplayConfig>) => {
|
||||
onChange(columns.map(c => {
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => {
|
||||
if (c.id !== colId) return c
|
||||
return { ...c, extDisplay: { displayMode: 'tag', ...(c.extDisplay || {}), ...patch } }
|
||||
}))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const resetExtDisplay = useCallback((colId: string) => {
|
||||
onChange(columns.map(c => {
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => {
|
||||
if (c.id !== colId) return c
|
||||
const { extDisplay, ...rest } = c
|
||||
return rest
|
||||
}))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const updateCandleConfig = useCallback((colId: string, patch: Partial<CandleColumnConfig>) => {
|
||||
onChange(columns.map(c => {
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => {
|
||||
if (c.id !== colId) return c
|
||||
return { ...c, candleConfig: { ...c.candleConfig, ...patch } }
|
||||
}))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const resetCandleConfig = useCallback((colId: string) => {
|
||||
onChange(columns.map(c => {
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => {
|
||||
if (c.id !== colId) return c
|
||||
const { candleConfig, ...rest } = c
|
||||
return rest
|
||||
}))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const updateIntradayConfig = useCallback((colId: string, patch: Partial<IntradayColumnConfig>) => {
|
||||
onChange(columns.map(c => {
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => {
|
||||
if (c.id !== colId) return c
|
||||
return { ...c, intradayConfig: { ...c.intradayConfig, ...patch } }
|
||||
}))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const resetIntradayConfig = useCallback((colId: string) => {
|
||||
onChange(columns.map(c => {
|
||||
const cols = columnsRef.current
|
||||
onChangeRef.current(cols.map(c => {
|
||||
if (c.id !== colId) return c
|
||||
const { intradayConfig, ...rest } = c
|
||||
return rest
|
||||
}))
|
||||
}, [columns, onChange])
|
||||
}, [])
|
||||
|
||||
const toggleGroup = useCallback((groupId: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
@@ -295,7 +321,7 @@ export function ListColumnCustomizer({
|
||||
}, [])
|
||||
|
||||
const extTables = extSchema.data?.items ?? []
|
||||
const extTableLabelMap = new Map(extTables.map(t => [t.id, t.label]))
|
||||
const extTableLabelMap = useMemo(() => new Map(extTables.map(t => [t.id, t.label])), [extTables])
|
||||
|
||||
const query = searchQuery.trim().toLowerCase()
|
||||
const filteredGroups = useMemo(() => {
|
||||
@@ -722,7 +748,7 @@ export function ListColumnCustomizer({
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
className={`absolute inset-0 bg-black/50 ${disableBackdropBlur ? '' : 'backdrop-blur-sm'}`}
|
||||
{...backdrop}
|
||||
/>
|
||||
<motion.div
|
||||
@@ -768,10 +794,10 @@ export function ListColumnCustomizer({
|
||||
onConfig={setConfigOpenId}
|
||||
configOpen={configOpenId === col.id}
|
||||
extTableLabel={col.source.type === 'ext' ? (extTableLabelMap.get(col.source.configId) || col.source.configId) : ''}
|
||||
extConfig={renderExtConfig(col)}
|
||||
candleConfig={renderCandleConfig(col)}
|
||||
intradayConfig={renderIntradayConfig(col)}
|
||||
strategiesConfig={renderStrategiesConfig(col)}
|
||||
extConfig={() => renderExtConfig(col)}
|
||||
candleConfig={() => renderCandleConfig(col)}
|
||||
intradayConfig={() => renderIntradayConfig(col)}
|
||||
strategiesConfig={() => renderStrategiesConfig(col)}
|
||||
showStandaloneToggle={showStandaloneToggle}
|
||||
onToggleStandalone={toggleStandalone}
|
||||
/>
|
||||
|
||||
@@ -272,6 +272,7 @@ export function StockInfoBar({ symbol, name, stockInfo, rows, fields, onFieldsCh
|
||||
builtinSectionLabel="可选指标"
|
||||
extColumnAlign="left"
|
||||
showStandaloneToggle
|
||||
disableBackdropBlur
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user