import type { FlexProps } from '@chakra-ui/react'; import { Box, type ButtonProps, Checkbox, Flex, Input, Menu, MenuButton, MenuItem, type MenuItemProps, MenuList, useDisclosure } from '@chakra-ui/react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import MyTag from '../Tag/index'; import MyIcon from '../Icon'; import MyAvatar from '../Avatar'; import { useTranslation } from 'next-i18next'; import type { useScrollPagination } from '../../../hooks/useScrollPagination'; import MyDivider from '../MyDivider'; import { shadowLight } from '../../../styles/theme'; import { isArray } from 'lodash'; import { useMount } from 'ahooks'; const menuItemStyles: MenuItemProps = { borderRadius: 'sm', py: 2, display: 'flex', alignItems: 'center', _hover: { backgroundColor: 'myGray.100' }, _notLast: { mb: 2 } }; export type SelectProps = { list: { icon?: string; label: string | React.ReactNode; value: T; }[]; value: T[]; isSelectAll: boolean; setIsSelectAll?: React.Dispatch>; placeholder?: string; itemWrap?: boolean; onSelect: (val: T[]) => void; closeable?: boolean; isDisabled?: boolean; ScrollData?: ReturnType['ScrollData']; formLabel?: string; formLabelFontSize?: string; inputValue?: string; setInputValue?: (val: string) => void; tagStyle?: FlexProps; } & Omit; const MultipleSelect = ({ value = [], placeholder, list = [], onSelect, closeable = false, itemWrap = true, ScrollData, isSelectAll, setIsSelectAll, isDisabled = false, formLabel, formLabelFontSize = 'sm', inputValue, setInputValue, tagStyle, ...props }: SelectProps) => { const SearchInputRef = useRef(null); const tagsContainerRef = useRef(null); const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); const canInput = setInputValue !== undefined; type SelectedItemType = { icon?: string; label: string | React.ReactNode; value: T; }; const [visibleItems, setVisibleItems] = useState([]); const [overflowItems, setOverflowItems] = useState([]); const selectedItems = useMemo(() => { if (!value || !isArray(value)) return []; return value.map((val) => { const listItem = list.find((item) => item.value === val); return listItem || { value: val, label: String(val) }; }); }, [value, list]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Backspace' && (!inputValue || inputValue === '')) { const newValue = [...value]; newValue.pop(); onSelect(newValue); } }, [inputValue, value, onSelect] ); useEffect(() => { if (!isOpen) { setInputValue?.(''); } }, [isOpen]); const onclickItem = useCallback( (val: T) => { if (isSelectAll) { onSelect(list.map((item) => item.value).filter((i) => i !== val)); setIsSelectAll?.(false); return; } if (value.includes(val)) { onSelect(value.filter((i) => i !== val)); } else { onSelect([...value, val]); } }, [isSelectAll, value, onSelect, list, setIsSelectAll] ); const onSelectAll = useCallback(() => { onSelect(isSelectAll ? [] : list.map((item) => item.value)); setIsSelectAll?.((state) => !state); }, [isSelectAll, onSelect, list, setIsSelectAll]); // 动态长度计算器 - 计算一行能展示多少个tag,剩余用+n表示 const calculateLayout = useCallback(() => { if (!tagsContainerRef.current || selectedItems.length === 0) { setVisibleItems(selectedItems); setOverflowItems([]); return; } const containerWidth = tagsContainerRef.current.offsetWidth; const tagGap = 4; // tag之间的gap const overflowIndicatorWidth = 30; // "+n" 宽度 const formLabelWidth = formLabel ? formLabel.length * 8 + 20 : 0; // 实际可用宽度 const availableWidth = containerWidth - formLabelWidth - 10; // 如果只有一个项目,直接显示 if (selectedItems.length === 1) { setVisibleItems(selectedItems); setOverflowItems([]); return; } // 创建临时元素来测量每个tag的实际宽度 const measureTagWidth = (item: any): number => { // 如果有tagStyle.w,优先使用 if (tagStyle?.w) { return typeof tagStyle.w === 'number' ? tagStyle.w : parseInt(String(tagStyle.w)) || 60; } // 否则根据文本长度估算(更精确) const text = String(item.label || item.value); const baseWidth = 16; // 基础padding const charWidth = 8; // 每个字符约8px const closeIconWidth = closeable ? 20 : 0; // 关闭按钮宽度 return baseWidth + text.length * charWidth + closeIconWidth; }; // 确保至少显示1个tag const firstTagWidth = measureTagWidth(selectedItems[0]); // 如果连第一个tag都放不下,也要强制显示 if (availableWidth < firstTagWidth) { setVisibleItems([selectedItems[0]]); setOverflowItems(selectedItems.slice(1)); return; } // 精确计算每个tag的宽度 let usedWidth = 0; let visibleCount = 0; for (let i = 0; i < selectedItems.length; i++) { const currentTagWidth = measureTagWidth(selectedItems[i]); const currentGap = i > 0 ? tagGap : 0; const remainingItems = selectedItems.length - i - 1; const needsOverflow = remainingItems > 0; const overflowSpace = needsOverflow ? overflowIndicatorWidth + tagGap : 0; const totalNeeded = usedWidth + currentTagWidth + currentGap + overflowSpace; if (totalNeeded <= availableWidth) { usedWidth += currentTagWidth + currentGap; visibleCount = i + 1; } else { break; } } // 保证至少显示1个tag if (visibleCount === 0) { visibleCount = 1; } setVisibleItems(selectedItems.slice(0, visibleCount)); setOverflowItems(selectedItems.slice(visibleCount)); }, [closeable, formLabel, selectedItems, tagStyle?.w]); // 动态监听容器宽度变化并重新计算布局 useEffect(() => { if (!tagsContainerRef.current) return; // 创建 ResizeObserver 监听容器宽度变化 const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { // 当容器宽度发生变化时,触发重新计算 requestAnimationFrame(() => { calculateLayout(); }); } }); // 开始监听容器 resizeObserver.observe(tagsContainerRef.current); // 初始计算 requestAnimationFrame(() => { calculateLayout(); }); // 清理监听器 return () => { resizeObserver.disconnect(); }; }, [calculateLayout]); // 当选中项目、样式等发生变化时重新计算 useEffect(() => { requestAnimationFrame(() => { calculateLayout(); }); }, [calculateLayout]); const ListRender = useMemo(() => { return ( <> {list.map((item, i) => { const isSelected = isSelectAll || value.includes(item.value); return ( { e.stopPropagation(); e.preventDefault(); onclickItem(item.value); }} whiteSpace={'pre-wrap'} fontSize={'sm'} gap={2} {...menuItemStyles} > {item.icon && } {item.label} ); })} ); }, [list, isSelectAll, value, onclickItem]); return ( {formLabel && ( {formLabel} )} {value.length === 0 && placeholder ? ( {placeholder} ) : ( {(!isOpen || !canInput) && (isSelectAll ? ( {t('common:All')} ) : ( <> {visibleItems.map((item, i) => ( {item.label} {closeable && ( { e.stopPropagation(); e.preventDefault(); onclickItem(item.value); }} /> )} ))} {overflowItems.length > 0 && ( +{overflowItems.length} )} ))} {canInput && isOpen && ( setInputValue?.(e.target.value)} onKeyDown={handleKeyDown} ref={SearchInputRef} autoFocus onBlur={() => { setTimeout(() => { SearchInputRef?.current?.focus(); }, 0); }} h={6} variant={'unstyled'} border={'none'} /> )} )} { e.stopPropagation(); e.preventDefault(); onSelectAll(); }} whiteSpace={'pre-wrap'} fontSize={'sm'} gap={2} mb={1} {...menuItemStyles} > {t('common:All')} {ScrollData ? {ListRender} : ListRender} ); }; export default MultipleSelect; export const useMultipleSelect = (defaultValue: T[] = [], defaultSelectAll = false) => { const [value, setValue] = useState(defaultValue); const [isSelectAll, setIsSelectAll] = useState(defaultSelectAll); return { value, setValue, isSelectAll, setIsSelectAll }; };