diff --git a/frontend/src/components/ListColumnCustomizer.tsx b/frontend/src/components/ListColumnCustomizer.tsx index 0998e08..5eb25aa 100644 --- a/frontend/src/components/ListColumnCustomizer.tsx +++ b/frontend/src/components/ListColumnCustomizer.tsx @@ -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, - {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>(new Set()) const [configOpenId, setConfigOpenId] = useState(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) => { - 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) => { - 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) => { - 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({ renderExtConfig(col)} + candleConfig={() => renderCandleConfig(col)} + intradayConfig={() => renderIntradayConfig(col)} + strategiesConfig={() => renderStrategiesConfig(col)} showStandaloneToggle={showStandaloneToggle} onToggleStandalone={toggleStandalone} /> diff --git a/frontend/src/components/StockInfoBar.tsx b/frontend/src/components/StockInfoBar.tsx index 1063249..a5f643f 100644 --- a/frontend/src/components/StockInfoBar.tsx +++ b/frontend/src/components/StockInfoBar.tsx @@ -272,6 +272,7 @@ export function StockInfoBar({ symbol, name, stockInfo, rows, fields, onFieldsCh builtinSectionLabel="可选指标" extColumnAlign="left" showStandaloneToggle + disableBackdropBlur /> )