From 993f01a30179d45418e41af0f3652a18ca71216e Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 2 Jul 2025 11:37:12 +0800 Subject: [PATCH] fix: multiple select ux --- src/components/seal-form/simple-select.tsx | 147 ++++++++++++++++-- .../components/usage-inner/filter-bar.tsx | 6 - 2 files changed, 132 insertions(+), 21 deletions(-) diff --git a/src/components/seal-form/simple-select.tsx b/src/components/seal-form/simple-select.tsx index 30ee9321..777b8328 100644 --- a/src/components/seal-form/simple-select.tsx +++ b/src/components/seal-form/simple-select.tsx @@ -2,7 +2,7 @@ import { useIntl } from '@umijs/max'; import type { SelectProps } from 'antd'; import { Checkbox, Select, Tag } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import AutoTooltip from '../auto-tooltip'; @@ -44,7 +44,7 @@ const TagWrapper = styled(Tag)` const SimpleSelect: React.FC = (props) => { const intl = useIntl(); - const { options, ...restProps } = props; + const { options = [], ...restProps } = props; const [allSelection, setAllSelection] = React.useState<{ checked: boolean; @@ -53,6 +53,13 @@ const SimpleSelect: React.FC = (props) => { checked: false, indeterminate: false }); + const [searchValue, setSearchValue] = React.useState(''); + const [optionsList, setOptionsList] = React.useState(options || []); + const selectRef = React.useRef(null); + + useEffect(() => { + setOptionsList(options || []); + }, [options]); const optionRender = (option: any, info: any) => { const { value, label } = option; @@ -70,13 +77,30 @@ const SimpleSelect: React.FC = (props) => { const handleOnCheckboxChange = (e: CheckboxChangeEvent) => { const isChecked = e.target.checked; - const allValues = options?.map((opt: any) => opt.value) || []; + const allValues = optionsList?.map((opt: any) => opt.value) || []; + + console.log('isChecked', isChecked, allValues); + setAllSelection({ checked: isChecked, indeterminate: false }); - restProps.onChange?.(isChecked ? allValues : [], options || []); + let allSelectedValues = [...(restProps.value || [])]; + + if (isChecked) { + // Select all options + allSelectedValues = Array.from( + new Set([...allSelectedValues, ...allValues]) + ); + } else { + // Deselect all options + allSelectedValues = allSelectedValues.filter( + (value) => !allValues.includes(value) + ); + } + + restProps.onChange?.(allSelectedValues, optionsList || []); }; const dropdownRender = (originPanel: React.ReactNode) => { @@ -100,7 +124,7 @@ const SimpleSelect: React.FC = (props) => { const handleOnChange = (value: any, option: any) => { const selectedValues = Array.isArray(value) ? value : [value]; - const allSelected = options?.map((opt: any) => opt.value) || []; + const allSelected = optionsList?.map((opt: any) => opt.value) || []; const isAllSelected = selectedValues.length === allSelected?.length; setAllSelection({ @@ -111,6 +135,42 @@ const SimpleSelect: React.FC = (props) => { restProps.onChange?.(selectedValues, option); }; + const filterOption = (inputValue: string, option: any) => { + if (!option || !option.label) return false; + return option.label.toLowerCase().includes(inputValue.toLowerCase()); + }; + + const checkAllSelection = (list: Global.BaseOption[]) => { + if ( + !restProps.value || + !Array.isArray(restProps.value) || + list.length === 0 + ) { + setAllSelection({ + checked: false, + indeterminate: false + }); + return; + } + const selectedValues = new Set(restProps.value); + const allValues = list?.map((opt: any) => opt.value) || []; + + const isAllSelected = allValues.every((val: any) => + selectedValues.has(val) + ); + + const isSomeSelected = allValues.some((val: any) => + selectedValues.has(val) + ); + + console.log('isAllSelected', isAllSelected, selectedValues, allValues); + + setAllSelection({ + checked: isAllSelected, + indeterminate: isSomeSelected && !isAllSelected + }); + }; + const TagRender = (props: any) => { const { label } = props; const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label; @@ -130,17 +190,74 @@ const SimpleSelect: React.FC = (props) => { ); }; + const handleOnSearch = (value: string) => { + if (restProps.onSearch) { + restProps.onSearch(value); + } else { + const filteredOptions = options?.filter((option: any) => + option.label.toLowerCase().includes(value.toLowerCase()) + ) as Global.BaseOption[]; + setOptionsList(filteredOptions || []); + checkAllSelection(filteredOptions || []); + } + }; + + const handleOnBlur = (e: any) => { + restProps.onBlur?.(e); + }; + + const handleOnFocus = (e: any) => { + restProps.onFocus?.(e); + }; + + const handleOnOpenChange = (open: boolean) => { + if (!open) { + checkAllSelection(options as Global.BaseOption[]); + setOptionsList(options || []); + } + }; + + useEffect(() => { + const input = selectRef.current?.querySelector?.('input'); + + if (!input) return; + + const handler = (event: KeyboardEvent) => { + if ( + event.key === 'Backspace' && + (input as HTMLInputElement).value === '' + ) { + event.stopPropagation(); + event.preventDefault(); + } + }; + + input.addEventListener('keydown', handler); + + return () => { + input.removeEventListener('keydown', handler); + }; + }, [selectRef.current]); + return ( - +
+ +
); }; diff --git a/src/pages/dashboard/components/usage-inner/filter-bar.tsx b/src/pages/dashboard/components/usage-inner/filter-bar.tsx index 24c866ca..7adfed2b 100644 --- a/src/pages/dashboard/components/usage-inner/filter-bar.tsx +++ b/src/pages/dashboard/components/usage-inner/filter-bar.tsx @@ -60,10 +60,6 @@ const FilterBar: React.FC = (props) => { const intl = useIntl(); - const filterOptions = (inputValue: any, option: any) => { - return option.label?.toLowerCase().includes(inputValue.toLowerCase()); - }; - return (
@@ -86,7 +82,6 @@ const FilterBar: React.FC = (props) => { mode="multiple" options={userList} maxTagCount={0} - filterOption={filterOptions} placeholder={intl.formatMessage({ id: 'dashboard.usage.selectuser' })} @@ -100,7 +95,6 @@ const FilterBar: React.FC = (props) => { mode="multiple" options={modelList} maxTagCount={0} - filterOption={filterOptions} placeholder={intl.formatMessage({ id: 'dashboard.usage.selectmodel' })}