import { useEffect, useMemo, useRef, useState } from 'react'; import { Check, ChevronDown, Filter, Plus, RotateCcw, Search, X } from 'lucide-react'; import type { AssetAnalysisCell, AssetAnalysisDimension as Dimension } from '../../../../shared/assets/analytics'; import { ANALYSIS_META, DIMENSION_LABELS, filterCustomCells, getDimensionValueLabel, type AnalysisFilterValues, type AnalysisView } from '../../analytics-model'; type Props = { view: AnalysisView; cells: AssetAnalysisCell[]; available: Dimension[]; dimensions: Dimension[]; setDimensions: (value: Dimension[]) => void; filters: AnalysisFilterValues; setFilters: (value: AnalysisFilterValues) => void; reset: () => void; }; export function AnalysisControls({ view, cells, available, dimensions, setDimensions, filters, setFilters, reset }: Props) { const root = useRef(null); const triggerRef = useRef(null); const [position, setPosition] = useState({ top: 0, left: 0 }); const [editor, setEditor] = useState(null); const [draft, setDraft] = useState([]); const [query, setQuery] = useState(''); const activeCount = Object.values(filters).filter(values => values?.length).length; useEffect(() => { if (!editor) return; const outside = (event: PointerEvent) => { if (!root.current?.contains(event.target as Node)) setEditor(null); }; document.addEventListener('pointerdown', outside); return () => document.removeEventListener('pointerdown', outside); }, [editor]); const metric = ANALYSIS_META[view].metric; const options = useMemo(() => { if (!editor) return []; const otherFilters = { ...filters }; delete otherFilters[editor]; const eligible = filterCustomCells(cells, otherFilters, metric); const counts = new Map(); eligible.forEach(cell => counts.set(cell[editor], (counts.get(cell[editor]) || 0) + cell.quantity)); for (const value of filters[editor] || []) if (!counts.has(value)) counts.set(value, 0); return [...counts].map(([value, count]) => ({ value, count, label: getDimensionValueLabel(editor, value) })) .sort((a,b) => b.count - a.count || a.label.localeCompare(b.label, 'zh-CN')) .filter(option => option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase())); }, [cells, editor, filters, metric, query]); const closeEditor = () => { setEditor(null); triggerRef.current?.focus(); }; const open = (dimension: Dimension, trigger: HTMLButtonElement) => { triggerRef.current = trigger; if (editor === dimension) { setEditor(null); return; } const bounds = root.current!.getBoundingClientRect(); const button = trigger.getBoundingClientRect(); setPosition({ top: button.bottom - bounds.top + 5, left: Math.max(8, Math.min(button.left - bounds.left, bounds.width - 338)) }); setEditor(dimension); setDraft(filters[dimension] || []); setQuery(''); }; const apply = () => { if (!editor) return; const next = { ...filters }; if (draft.length) next[editor] = draft; else delete next[editor]; setFilters(next); closeEditor(); }; return

分析维度

点名称组合统计 · 点箭头搜索筛选
{available.map(dimension => { const index = dimensions.indexOf(dimension); const selected = filters[dimension] || []; const filterLabel = selected.map(value => getDimensionValueLabel(dimension, value)).join('、'); return
; })}
{activeCount > 0 &&
{activeCount} 个维度已筛选 · 同维度多选为“或”,不同维度为“且”
} {editor &&
{ if (event.key === 'Escape') { event.stopPropagation(); closeEditor(); } }}>
{DIMENSION_LABELS[editor]}搜索与筛选
已选 {draft.length} 项
{options.map(option => )} {!options.length &&

没有匹配选项,可调整搜索或其他筛选。

}
未选择时不限制此维度
}
; }