diff --git a/src/components/cascader-view/cascader-view.tsx b/src/components/cascader-view/cascader-view.tsx index 206f782ecc..e7896be4b9 100644 --- a/src/components/cascader-view/cascader-view.tsx +++ b/src/components/cascader-view/cascader-view.tsx @@ -13,6 +13,7 @@ import Skeleton from '../skeleton' import { useUpdateEffect } from 'ahooks' import { useFieldNames } from '../../hooks' import type { FieldNamesType, BaseOptionType } from '../../hooks' +import { cloneDeep } from 'lodash' const classPrefix = `adm-cascader-view` @@ -32,14 +33,16 @@ export type CascaderValueExtend = { export type CascaderViewProps = { options: CascaderOption[] - value?: CascaderValue[] - defaultValue?: CascaderValue[] - onChange?: (value: CascaderValue[], extend: CascaderValueExtend) => void + value?: CascaderValue[] | any + defaultValue?: CascaderValue[] | any + onChange?: (value: CascaderValue[] | any, extend: CascaderValueExtend) => void placeholder?: string | ((index: number) => string) onTabsChange?: (index: number) => void activeIcon?: ReactNode loading?: boolean + multiple?: boolean fieldNames?: FieldNamesType + activeIconSetPath?: boolean } & NativeProps<'--height'> const defaultProps = { @@ -48,7 +51,6 @@ const defaultProps = { export const CascaderView: FC = p => { const props = mergeProps(defaultProps, p) - const { locale } = useConfig() const [labelName, valueName, childrenName, disabledName] = useFieldNames( props.fieldNames @@ -57,23 +59,76 @@ export const CascaderView: FC = p => { valueName, childrenName, }) - const [value, setValue] = usePropsValue({ ...props, onChange: val => { - props.onChange?.(val, generateValueExtend(val)) + props.onChange?.( + val, + props.multiple ? generateValueExtend(val as string[]) : {} + ) }, }) const [tabActiveIndex, setTabActiveIndex] = useState(0) - const levels = useMemo(() => { + const extractLevelsForMultiple = ( + value: any[], + options: CascaderOption[], + valueName: string, + childrenName: string + ) => { + const ret: { + selected: CascaderOption | undefined + options: CascaderOption[] + }[] = [] + + let currentOptions = options + let reachedEnd = false + + for (const v of value) { + let target: CascaderOption | undefined + ;(v as string[]).forEach(e => { + const temp = currentOptions.find(option => + e.includes(option[valueName]) + ) + if (temp) { + target = temp + } + }) + ret.push({ + selected: target, + options: currentOptions, + }) + if (!target || !target[childrenName]) { + reachedEnd = true + break + } + currentOptions = target[childrenName] + } + + if (!reachedEnd) { + ret.push({ + selected: undefined, + options: currentOptions, + }) + } + + return ret + } + + const extractLevelsForSingle = ( + value: any[], + options: CascaderOption[], + valueName: string, + childrenName: string + ) => { const ret: { selected: CascaderOption | undefined options: CascaderOption[] }[] = [] - let currentOptions = props.options + let currentOptions = options let reachedEnd = false + for (const v of value) { const target = currentOptions.find(option => option[valueName] === v) ret.push({ @@ -86,20 +141,44 @@ export const CascaderView: FC = p => { } currentOptions = target[childrenName] } + if (!reachedEnd) { ret.push({ selected: undefined, options: currentOptions, }) } + return ret + } + + const levels = useMemo(() => { + if (props.multiple) { + return extractLevelsForMultiple( + value, + props.options, + valueName, + childrenName + ) + } else { + return extractLevelsForSingle( + value, + props.options, + valueName, + childrenName + ) + } }, [value, props.options]) useUpdateEffect(() => { - props.onTabsChange?.(tabActiveIndex) + if (!props.multiple) { + props.onTabsChange?.(tabActiveIndex) + } }, [tabActiveIndex]) useEffect(() => { - setTabActiveIndex(levels.length - 1) + if (!props.multiple) { + setTabActiveIndex(levels.length - 1) + } }, [value]) useEffect(() => { const max = levels.length - 1 @@ -108,12 +187,37 @@ export const CascaderView: FC = p => { } }, [tabActiveIndex, levels]) - const onItemSelect = (selectValue: CascaderValue, depth: number) => { + const onItemSelect = ( + selectValue: CascaderValue | CascaderValue[], + depth: number + ) => { const next = value.slice(0, depth) if (selectValue !== undefined) { next[depth] = selectValue } - setValue(next) + if (props.multiple) { + const cloneValue = cloneDeep(value) + cloneValue[depth] = next[depth] + setValue(cloneValue) + } else { + setValue(next) + } + } + + const setPath = (selectValue: string, depth: number) => { + const currentPath = cloneDeep(value) + if (selectValue !== undefined) { + if (!currentPath[depth]) { + currentPath[depth] = [] + } + currentPath[depth] = (currentPath[depth] as string[]).filter( + (item: string) => { + return item !== selectValue + } + ) + currentPath[depth].push(selectValue) + } + setValue(currentPath) } const whetherLoading = (options: T) => @@ -143,8 +247,8 @@ export const CascaderView: FC = p => { {selected ? selected[labelName] : typeof placeholder === 'function' - ? placeholder(index) - : placeholder} + ? placeholder(index) + : placeholder} } forceRender @@ -171,14 +275,31 @@ export const CascaderView: FC = p => { ) : ( - onItemSelect(selectValue[0], index) - } + value={props.multiple ? value[index] : [value[index]]} + onChange={selectValue => { + if (props.multiple) { + onItemSelect(selectValue, index) + } else { + onItemSelect(selectValue[0], index) + } + }} + multiple={props.multiple} + activeSetPathMiddleware={{ + index, + activeIconSetPath: props.activeIconSetPath, + setPath, + }} activeIcon={props.activeIcon} > {level.options.map(option => { - const active = value[index] === option[valueName] + let active + if (props.multiple) { + active = (value[index] as Array)?.includes( + option[valueName] + ) + } else { + active = value[index] === option[valueName] + } return ( { + const option = currentOptions.find( + (opt: { value: any }) => opt.value === value + ) + if (option) { + isFinish = false + helper(option.children || [], remainingData, path.concat(option.value)) + } + }) + + if (isFinish) { + if (Array.isArray(path)) { + result.push(path) + return + } + } + } + + helper(options, data, []) + return result +} + export type CascaderProps = { + activeIconSetPath?: boolean options: CascaderOption[] - value?: CascaderValue[] + multiple?: boolean + value?: CascaderValue[] | CascaderValue[][] defaultValue?: CascaderValue[] placeholder?: string - onSelect?: (value: CascaderValue[], extend: CascaderValueExtend) => void - onConfirm?: (value: CascaderValue[], extend: CascaderValueExtend) => void + onSelect?: ( + value: CascaderValue[] | CascaderValue[][], + extend: CascaderValueExtend + ) => void + onConfirm?: ( + value: CascaderValue[] | CascaderValue[][], + extend: CascaderValueExtend + ) => void onCancel?: () => void onClose?: () => void visible?: boolean @@ -68,6 +121,23 @@ const defaultProps = { forceRender: false, } +/** + * @description 树转化成二维数组 + */ +const dataPreview = (data: Array) => { + const temp: any[] = [] + cloneDeep(data).forEach(item => { + item.forEach((e: any, index: number) => { + if (!temp[index]) { + temp[index] = [e] + } else { + temp[index].push(e) + } + }) + }) + return temp +} + export const Cascader = forwardRef((p, ref) => { const { locale } = useConfig() const props = mergeProps( @@ -107,7 +177,24 @@ export const Cascader = forwardRef((p, ref) => { const [value, setValue] = usePropsValue({ ...props, onChange: val => { - props.onConfirm?.(val, generateValueExtend(val)) + if (props.multiple) { + const hash: Record = {} + let result = generatePaths( + props.options, + val as Array + ) + result = result.filter(item => { + if (hash[JSON.stringify(item)]) { + return false + } else { + hash[JSON.stringify(item)] = true + return true + } + }) + props.onConfirm?.(result, {} as any) + } else { + props.onConfirm?.(val, generateValueExtend(val as CheckListValue[])) + } }, }) @@ -117,11 +204,23 @@ export const Cascader = forwardRef((p, ref) => { childrenName, }) - const [innerValue, setInnerValue] = useState(value) + let valueTemp + if (props.multiple) { + valueTemp = dataPreview(value as Array) + } else { + valueTemp = value as CascaderValue[] + } + const [innerValue, setInnerValue] = useState< + CascaderValue[] | CascaderValue[][] + >(valueTemp) useEffect(() => { if (!visible) { - setInnerValue(value) + if (props.multiple) { + setInnerValue(dataPreview(value as Array)) + } else { + setInnerValue(value) + } } }, [visible, value]) @@ -187,7 +286,10 @@ export const Cascader = forwardRef((p, ref) => { return ( <> {popupElement} - {props.children?.(generateValueExtend(value).items, actions)} + {props.children?.( + generateValueExtend(value as CheckListValue[]).items, + actions + )} ) }) diff --git a/src/components/cascader/index.en.md b/src/components/cascader/index.en.md index 78100ec70f..745bd14458 100644 --- a/src/components/cascader/index.en.md +++ b/src/components/cascader/index.en.md @@ -34,6 +34,7 @@ type CascaderValueExtend = { | Name | Description | Type | Default | | --- | --- | --- | --- | +| multiple | Whether to support selecting multiple options | `boolean` | false | | activeIcon | The icon displayed when selected | `ReactNode` | - | | cancelText | Text of the cancel button | `ReactNode` | `'取消'` | | children | Render function of the selected options | `(items: CascaderOption[], actions: CascaderActions) => ReactNode` | - | diff --git a/src/components/cascader/index.zh.md b/src/components/cascader/index.zh.md index 9310cb7bd1..6b7cdd330d 100644 --- a/src/components/cascader/index.zh.md +++ b/src/components/cascader/index.zh.md @@ -34,6 +34,7 @@ type CascaderValueExtend = { | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | +| multiple | 是否允许多选 | `boolean` | false | | activeIcon | 选中图标 | `ReactNode` | - | | cancelText | 取消按钮的文字 | `ReactNode` | `'取消'` | | children | 所选项的渲染函数 | `(items: CascaderOption[], actions: CascaderActions) => ReactNode` | - | diff --git a/src/components/cascader/prompt.tsx b/src/components/cascader/prompt.tsx index 6e31adb1ae..fa03e6c3b5 100644 --- a/src/components/cascader/prompt.tsx +++ b/src/components/cascader/prompt.tsx @@ -7,7 +7,7 @@ import type { CascaderProps, CascaderValue } from './index' export function prompt( props: Omit ) { - return new Promise(resolve => { + return new Promise(resolve => { const Wrapper: FC = () => { const [visible, setVisible] = useState(false) useEffect(() => { diff --git a/src/components/check-list/check-list-item.tsx b/src/components/check-list/check-list-item.tsx index dc17d9cca1..d9d6641208 100644 --- a/src/components/check-list/check-list-item.tsx +++ b/src/components/check-list/check-list-item.tsx @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck import React, { useContext } from 'react' import type { FC } from 'react' import List, { ListItemProps } from '../list' @@ -35,8 +37,26 @@ export const CheckListItem: FC = props => { const active = context.value.includes(props.value) const readOnly = props.readOnly || context.readOnly const defaultExtra = active ? context.activeIcon : null + + const { activeIconSetPath, index, setPath } = active + ? context.activeSetPathMiddleware + ? context.activeSetPathMiddleware + : {} + : {} const renderExtra = context.extra ? context.extra(active) : defaultExtra - const extra =
{renderExtra}
+ const extra = ( +
{ + if (activeIconSetPath) { + setPath(props.value, index) + event.stopPropagation() + } + }} + > + {renderExtra} +
+ ) return withNativeProps( props, diff --git a/src/components/check-list/check-list.tsx b/src/components/check-list/check-list.tsx index 74b6ec01d9..7c2256ca69 100644 --- a/src/components/check-list/check-list.tsx +++ b/src/components/check-list/check-list.tsx @@ -22,6 +22,13 @@ export type CheckListProps = Pick & { disabled?: boolean readOnly?: boolean children?: ReactNode + activeSetPathMiddleware: { + activeIconSetPath: any + value?: any + event?: any + setPath: any + index: any + } } & NativeProps const defaultProps = { @@ -38,7 +45,7 @@ export const CheckList: FC = props => { function check(val: CheckListValue) { if (mergedProps.multiple) { - setValue([...value, val]) + setValue([...(value as CheckListValue[]), val]) } else { setValue([val]) } @@ -48,7 +55,8 @@ export const CheckList: FC = props => { setValue(value.filter(item => item !== val)) } - const { activeIcon, extra, disabled, readOnly } = mergedProps + const { activeIcon, extra, disabled, readOnly, activeSetPathMiddleware } = + mergedProps return ( = props => { extra, disabled, readOnly, + activeSetPathMiddleware, }} > {withNativeProps( diff --git a/src/components/check-list/context.tsx b/src/components/check-list/context.tsx index 4af764ac20..0ffbcfaa1d 100644 --- a/src/components/check-list/context.tsx +++ b/src/components/check-list/context.tsx @@ -10,4 +10,11 @@ export const CheckListContext = createContext<{ extra?: (active: boolean) => ReactNode disabled?: boolean readOnly?: boolean + activeSetPathMiddleware: { + activeIconSetPath: any + value?: any + event?: any + setPath: any + index: any + } } | null>(null)