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