From eaa464af393239204fa03b540dcbe2afcc01cf5d Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Wed, 22 Jan 2025 13:36:02 +0800 Subject: [PATCH 01/11] refactor: cascader --- src/packages/cascader/cascader-origin.tsx | 477 ++++++++++++++++++ src/packages/cascader/cascader.tsx | 577 ++++++---------------- src/packages/cascader/demo.tsx | 10 +- src/packages/cascader/helper.ts | 79 ++- src/packages/cascader/tree.ts | 33 ++ src/packages/cascader/types.ts | 5 + src/packages/tabs/tabs.tsx | 2 +- src/utils/is-empty.ts | 6 + 8 files changed, 706 insertions(+), 483 deletions(-) create mode 100644 src/packages/cascader/cascader-origin.tsx create mode 100644 src/utils/is-empty.ts diff --git a/src/packages/cascader/cascader-origin.tsx b/src/packages/cascader/cascader-origin.tsx new file mode 100644 index 0000000000..b3f80847dd --- /dev/null +++ b/src/packages/cascader/cascader-origin.tsx @@ -0,0 +1,477 @@ +import React, { + ForwardRefRenderFunction, + PropsWithChildren, + isValidElement, + useState, + useEffect, + ReactNode, + useImperativeHandle, +} from 'react' +import classNames from 'classnames' +import { Loading, Checklist } from '@nutui/icons-react' +import { Popup } from '@/packages/popup/popup' +import { Tabs } from '@/packages/tabs/tabs' +import { convertListToOptions } from './helper' +import { + CascaderPane, + CascaderOption, + CascaderValue, + CascaderOptionKey, + CascaderFormat, CascaderActions, +} from './types' +import Tree from './tree' +import { ComponentDefaults } from '@/utils/typings' +import { usePropsValue } from '@/utils/use-props-value' +import { useConfig } from '@/packages/configprovider' + +export interface CascaderProps { + popup: boolean + visible: boolean + activeColor: string + activeIcon: string + options: CascaderOption[] + value?: CascaderValue + defaultValue?: CascaderValue + optionKey: CascaderOptionKey + format: Record + closeable: boolean + closeIconPosition: string + closeIcon: ReactNode + lazy: boolean + onLoad: (node: any, resolve: any) => void + onChange: (value: CascaderValue, params?: any) => void + onPathChange: (value: CascaderValue, params: any) => void +} + +const defaultProps = { + ...ComponentDefaults, + activeColor: '', + activeIcon: 'checklist', + popup: true, + options: [], + optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + format: {}, + closeable: false, + closeIconPosition: 'top-right', + closeIcon: 'close', + lazy: false, + onLoad: () => {}, + onClose: () => {}, + onChange: () => {}, + onPathChange: () => {}, +} as unknown as CascaderProps +const InternalCascader: ForwardRefRenderFunction< + unknown, + PropsWithChildren> +> = (props, ref) => { + const { locale } = useConfig() + const { + className, + style, + activeColor, + activeIcon, + popup, + popupProps = {}, + visible, + options, + value, + defaultValue, + optionKey, + format, + closeable, + closeIconPosition, + closeIcon, + lazy, + title, + left, + onLoad, + onClose, + onChange, + onPathChange, + } = { ...defaultProps, ...props } + + const [tabvalue, setTabvalue] = useState('c1') + const [optionsData, setOptionsData] = useState([]) + const isLazy = () => state.configs.lazy && Boolean(state.configs.onLoad) + + const [innerValue, setInnerValue] = usePropsValue({ + value, + defaultValue, + finalValue: defaultValue, + }) + const [innerVisible, setInnerVisible] = usePropsValue({ + value: visible, + defaultValue: undefined, + finalValue: false, + }) + const actions: CascaderActions = { + open: () => { + setInnerVisible(true) + }, + close: () => { + setInnerVisible(false) + }, + } + useImperativeHandle(ref, () => actions) + + const [state] = useState({ + optionsData: [] as any, + panes: [ + { + nodes: [] as any, + selectedNode: [] as CascaderOption | null, + paneKey: '', + }, + ], + tree: new Tree([], {}), + tabsCursor: 0, // 选中的tab项 + initLoading: false, + currentProcessNode: [] as CascaderOption | null, + configs: { + lazy, + onLoad, + optionKey, + format, + }, + lazyLoadMap: new Map(), + }) + + const classPrefix = classNames(`nut-cascader`) + const classesPane = classNames({ + [`${classPrefix}-pane`]: true, + }) + + useEffect(() => { + initData() + }, [options, format]) + + useEffect(() => { + syncValue() + }, [value]) + + const initData = async () => { + // 初始化开始处理数据 + state.lazyLoadMap.clear() + if (format && Object.keys(format).length > 0) { + state.optionsData = convertListToOptions( + options as CascaderOption[], + format as CascaderFormat + ) + } else { + state.optionsData = options + } + state.tree = new Tree(state.optionsData as CascaderOption[], { + value: state.configs.optionKey.valueKey, + text: state.configs.optionKey.textKey, + children: state.configs.optionKey.childrenKey, + }) + if (isLazy() && !state.tree.nodes.length) { + await invokeLazyLoad({ + root: true, + loading: true, + text: '', + value: '', + }) + } + state.panes = [ + { + nodes: state.tree.nodes, + selectedNode: null, + paneKey: 'c1', + }, + ] + syncValue() + setOptionsData(state.panes) + } + // 处理有默认值时的数据 + const syncValue = async () => { + const currentValue = innerValue + + if ( + currentValue === undefined || + ![defaultValue, value].includes(currentValue) || + !state.tree.nodes.length + ) { + return + } + + if (currentValue.length === 0) { + state.tabsCursor = 0 + return + } + + let needToSync = currentValue + + if (isLazy() && Array.isArray(currentValue) && currentValue.length) { + needToSync = [] + const parent: any = state.tree.nodes.find( + (node) => node.value === currentValue[0] + ) + + if (parent) { + needToSync = [parent.value] + state.initLoading = true + + const last = await currentValue + .slice(1) + .reduce(async (p: Promise, value) => { + const parent = await p + await invokeLazyLoad(parent) + const node: any = parent?.children?.find( + (item: any) => item.value === value + ) + if (node) { + needToSync.push(value) + } + return Promise.resolve(node) + }, Promise.resolve(parent)) + await invokeLazyLoad(last) + state.initLoading = false + } + } + + if (needToSync.length && [defaultValue, value].includes(currentValue)) { + const pathNodes = state.tree.getPathNodesByValue(needToSync) + pathNodes.forEach((node, index) => { + state.tabsCursor = index + // 当有默认值时,不触发 chooseItem 里的 emit 事件 + chooseItem(node, true) + }) + } + } + + const invokeLazyLoad = async (node?: CascaderOption | void) => { + if (!node) { + return + } + + if (!state.configs.onLoad) { + node.leaf = true + return + } + + if ( + state.tree.isLeaf(node, isLazy()) || + state.tree.hasChildren(node, isLazy()) + ) { + return + } + + node.loading = true + + const parent = node.root ? null : node + let lazyLoadPromise = state.lazyLoadMap.get(node) + + if (!lazyLoadPromise) { + lazyLoadPromise = new Promise((resolve) => { + // 外部必须resolve + state.configs.onLoad?.(node, resolve) + }) + state.lazyLoadMap.set(node, lazyLoadPromise) + } + + const nodes: CascaderOption[] | void = await lazyLoadPromise + + if (Array.isArray(nodes) && nodes.length > 0) { + state.tree.updateChildren(nodes, parent) + } else { + // 如果加载完成后没有提供子节点,作为叶子节点处理 + node.leaf = true + } + node.loading = false + state.lazyLoadMap.delete(node) + } + + const close = () => { + setInnerVisible(false) + onClose && onClose() + } + + const closePopup = () => { + close() + } + + /* type: 是否是静默模式,是的话不触发事件 + tabsCursor: tab的索引 */ + const chooseItem = async (node: CascaderOption, type: boolean) => { + if ((!type && node.disabled) || !state.panes[state.tabsCursor]) { + return + } + // 如果没有子节点 + if (state.tree.isLeaf(node, isLazy())) { + node.leaf = true + state.panes[state.tabsCursor].selectedNode = node + state.panes = state.panes.slice(0, (node.level as number) + 1) + if (!type) { + const pathNodes = state.panes.map((item) => item.selectedNode) + const optionParams = pathNodes.map((item: any) => item.value) + onChange(optionParams, pathNodes) + onPathChange?.(optionParams, pathNodes) + setInnerValue(optionParams) + } + setOptionsData(state.panes) + close() + return + } + // 如果有子节点,滑到下一个 + if (state.tree.hasChildren(node, isLazy())) { + const level = (node.level as number) + 1 + + state.panes[state.tabsCursor].selectedNode = node + state.panes = state.panes.slice(0, level) + state.tabsCursor = level + state.panes.push({ + nodes: node.children || [], + selectedNode: null, + paneKey: `c${state.tabsCursor + 1}`, + }) + setOptionsData(state.panes) + setTabvalue(`c${state.tabsCursor + 1}`) + + if (!type) { + const pathNodes = state.panes.map((item) => item.selectedNode) + const optionParams = pathNodes.map((item: any) => item?.value) + onPathChange?.(optionParams, pathNodes) + } + return + } + state.currentProcessNode = node + if (node.loading) { + return + } + + await invokeLazyLoad(node) + if (state.currentProcessNode === node) { + state.panes[state.tabsCursor].selectedNode = node + chooseItem(node, type) + } + setOptionsData(state.panes) + } + + const renderItem = (pane: any, node: any, index: number) => { + const nutCascaderItem = 'nut-cascader-item' + const checked = pane.selectedNode?.value === node.value + + const classes = classNames( + { + active: checked, + disabled: node.disabled, + }, + nutCascaderItem + ) + + const classesTitle = classNames({ + [`${nutCascaderItem}-title`]: true, + }) + + const renderIcon = () => { + if (!checked) return null + return isValidElement(activeIcon) ? ( + activeIcon + ) : ( + + ) + } + + return ( +
{ + chooseItem(node, false) + }} + > +
{node.text}
+ {node.loading ? ( + + ) : ( + renderIcon() + )} +
+ ) + } + + const renderTabs = () => { + return ( +
+ { + return optionsData.map((pane, index) => ( +
{ + setTabvalue(pane.paneKey) + state.tabsCursor = index + }} + className={`nut-tabs-titles-item ${ + tabvalue === pane.paneKey ? 'nut-tabs-titles-item-active' : '' + }`} + key={pane.paneKey} + > + + {!state.initLoading && + state.panes.length && + pane?.selectedNode?.text} + {!state.initLoading && + state.panes.length && + !pane?.selectedNode?.text && + `${locale.select}`} + {!(!state.initLoading && state.panes.length) && 'Loading...'} + + +
+ )) + }} + > + {!state.initLoading && state.panes.length ? ( + optionsData.map((pane) => ( + +
+ {pane.nodes?.map((node: any, index: number) => + renderItem(pane, node, index) + )} +
+
+ )) + ) : ( + +
+ + )} + +
+ ) + } + + return ( + <> + {popup ? ( + + {renderTabs()} + + ) : ( + renderTabs() + )} + + ) +} + +export const CascaderOrigin = React.forwardRef(InternalCascader) + +CascaderOrigin.displayName = 'NutCascader' diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index f6c4c2f647..04e14e1496 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -1,73 +1,31 @@ -import React, { - ForwardRefRenderFunction, - PropsWithChildren, - isValidElement, - useState, - useEffect, - ReactNode, - useImperativeHandle, -} from 'react' +import React, { ReactNode, useEffect, useMemo, useState } from 'react' +import { Checklist, Loading } from '@nutui/icons-react' import classNames from 'classnames' -import { Loading, Checklist } from '@nutui/icons-react' -import { Popup, PopupProps } from '@/packages/popup/popup' -import { Tabs } from '@/packages/tabs/tabs' -import { convertListToOptions } from './helper' -import { - CascaderPane, - CascaderOption, - CascaderValue, - CascaderOptionKey, - CascaderFormat, -} from './types' -import Tree from './tree' +import Tabs from '@/packages/tabs' +import Popup, { PopupProps } from '@/packages/popup' +import { CascaderValue, CascaderOptionKey, CascaderOption } from './types' import { ComponentDefaults } from '@/utils/typings' +import { CascaderProps } from '@/packages/cascader/cascader-origin' +import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' -import { useConfig } from '@/packages/configprovider' +import { isEmpty } from '@/utils/is-empty' +import { convertListToOptions, normalizeOptions } from '@/packages/cascader/helper' -export interface CascaderProps - extends Pick< - PopupProps, - | 'className' - | 'style' - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > { - popup: boolean - popupProps: Partial< - Omit< - PopupProps, - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > - > - visible: boolean // popup visible - activeColor: string - activeIcon: string +export interface CascaderPorps extends PopupProps { + visible: boolean + value: CascaderValue + defaultValue: CascaderValue options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue optionKey: CascaderOptionKey format: Record closeable: boolean - closeIconPosition: string closeIcon: ReactNode - lazy: boolean - onLoad: (node: any, resolve: any) => void - onChange: (value: CascaderValue, params?: any) => void + closeIconPosition: string + onLoad: (node: CascaderValue) => Promise + onChange: (value: CascaderValue, params: any) => void onPathChange: (value: CascaderValue, params: any) => void -} - -export type CascaderActions = { - open: () => void - close: () => void + onTabsChange: (index: number) => void + onClose: () => void } const defaultProps = { @@ -76,7 +34,7 @@ const defaultProps = { activeIcon: 'checklist', popup: true, options: [], - optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + optionKey: {}, format: {}, closeable: false, closeIconPosition: 'top-right', @@ -87,421 +45,176 @@ const defaultProps = { onChange: () => {}, onPathChange: () => {}, } as unknown as CascaderProps -const InternalCascader: ForwardRefRenderFunction< - unknown, - PropsWithChildren> -> = (props, ref) => { - const { locale } = useConfig() + +export const Cascader = (props: Partial) => { + const classPrefix = 'nut-cascader' + const classPane = `${classPrefix}-pane` const { - className, - style, activeColor, activeIcon, popup, - popupProps = {}, - visible, - options, - value, - defaultValue, + visible: outerVisible, + options: outerOptions, + value: outerValue, + defaultValue: outerDefaultValue, optionKey, format, closeable, closeIconPosition, closeIcon, lazy, - title, - left, onLoad, - onClose, - onChange, - onPathChange, - } = { ...defaultProps, ...props } + } = mergeProps(defaultProps, props) - const [tabvalue, setTabvalue] = useState('c1') - const [optionsData, setOptionsData] = useState([]) - const isLazy = () => state.configs.lazy && Boolean(state.configs.onLoad) + const [tabActiveIndex, setTabActiveIndex] = useState(0) - const [innerValue, setInnerValue] = usePropsValue({ - value, - defaultValue, - finalValue: defaultValue, - }) - const [innerVisible, setInnerVisible] = usePropsValue({ - value: visible, - defaultValue: undefined, - finalValue: false, - }) - const actions: CascaderActions = { - open: () => { - setInnerVisible(true) - }, - close: () => { - setInnerVisible(false) - }, - } - useImperativeHandle(ref, () => actions) + const options = useMemo(() => { + console.log(convertListToOptions(outerOptions, format)) + if (!isEmpty(format)) { + return convertListToOptions(outerOptions, format) + } + if (!isEmpty(optionKey)) { + return normalizeOptions(outerOptions, optionKey) + } + return outerOptions + }, [outerOptions, optionKey, format]) - const [state] = useState({ - optionsData: [] as any, - panes: [ - { - nodes: [] as any, - selectedNode: [] as CascaderOption | null, - paneKey: '', - }, - ], - tree: new Tree([], {}), - tabsCursor: 0, // 选中的tab项 - initLoading: false, - currentProcessNode: [] as CascaderOption | null, - configs: { - lazy, - onLoad, - optionKey, - format, + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + props.onChange?.(value, true) + props.onPathChange?.(value, true) }, - lazyLoadMap: new Map(), }) - const classPrefix = classNames(`nut-cascader`) - const classesPane = classNames({ - [`${classPrefix}-pane`]: true, - }) - - useEffect(() => { - initData() - }, [options, format]) - - useEffect(() => { - syncValue() - }, [value]) - - const initData = async () => { - // 初始化开始处理数据 - state.lazyLoadMap.clear() - if (format && Object.keys(format).length > 0) { - state.optionsData = convertListToOptions( - options as CascaderOption[], - format as CascaderFormat - ) - } else { - state.optionsData = options - } - state.tree = new Tree(state.optionsData as CascaderOption[], { - value: state.configs.optionKey.valueKey, - text: state.configs.optionKey.textKey, - children: state.configs.optionKey.childrenKey, - }) - if (isLazy() && !state.tree.nodes.length) { - await invokeLazyLoad({ - root: true, - loading: true, - text: '', - value: '', + const levels: any[] = useMemo(() => { + const next = [] + let end = false + let currentOptions = options + for (const val of value) { + const opt = currentOptions.find((o) => o.value === val) + next.push({ + selected: val, + pane: currentOptions, }) - } - state.panes = [ - { - nodes: state.tree.nodes, - selectedNode: null, - paneKey: 'c1', - }, - ] - syncValue() - setOptionsData(state.panes) - } - // 处理有默认值时的数据 - const syncValue = async () => { - const currentValue = innerValue - - if ( - currentValue === undefined || - ![defaultValue, value].includes(currentValue) || - !state.tree.nodes.length - ) { - return - } - - if (currentValue.length === 0) { - state.tabsCursor = 0 - return - } - - let needToSync = currentValue - - if (isLazy() && Array.isArray(currentValue) && currentValue.length) { - needToSync = [] - const parent: any = state.tree.nodes.find( - (node) => node.value === currentValue[0] - ) - - if (parent) { - needToSync = [parent.value] - state.initLoading = true - - const last = await currentValue - .slice(1) - .reduce(async (p: Promise, value) => { - const parent = await p - await invokeLazyLoad(parent) - const node: any = parent?.children?.find( - (item: any) => item.value === value - ) - if (node) { - needToSync.push(value) - } - return Promise.resolve(node) - }, Promise.resolve(parent)) - await invokeLazyLoad(last) - state.initLoading = false + if (opt?.children) { + currentOptions = opt.children + } else { + end = true + break } } - - if (needToSync.length && [defaultValue, value].includes(currentValue)) { - const pathNodes = state.tree.getPathNodesByValue(needToSync) - pathNodes.forEach((node, index) => { - state.tabsCursor = index - // 当有默认值时,不触发 chooseItem 里的 emit 事件 - chooseItem(node, true) - }) - } - } - - const invokeLazyLoad = async (node?: CascaderOption | void) => { - if (!node) { - return - } - - if (!state.configs.onLoad) { - node.leaf = true - return - } - - if ( - state.tree.isLeaf(node, isLazy()) || - state.tree.hasChildren(node, isLazy()) - ) { - return - } - - node.loading = true - - const parent = node.root ? null : node - let lazyLoadPromise = state.lazyLoadMap.get(node) - - if (!lazyLoadPromise) { - lazyLoadPromise = new Promise((resolve) => { - // 外部必须resolve - state.configs.onLoad?.(node, resolve) + if (!end) { + next.push({ + selected: null, + pane: currentOptions, }) - state.lazyLoadMap.set(node, lazyLoadPromise) } + return next + }, [value, options]) - const nodes: CascaderOption[] | void = await lazyLoadPromise - - if (Array.isArray(nodes) && nodes.length > 0) { - state.tree.updateChildren(nodes, parent) - } else { - // 如果加载完成后没有提供子节点,作为叶子节点处理 - node.leaf = true - } - node.loading = false - state.lazyLoadMap.delete(node) - } - - const close = () => { - setInnerVisible(false) - onClose && onClose() - } - - const closePopup = () => { - close() - } - - /* type: 是否是静默模式,是的话不触发事件 - tabsCursor: tab的索引 */ - const chooseItem = async (node: CascaderOption, type: boolean) => { - if ((!type && node.disabled) || !state.panes[state.tabsCursor]) { - return - } - // 如果没有子节点 - if (state.tree.isLeaf(node, isLazy())) { - node.leaf = true - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, (node.level as number) + 1) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item.value) - onChange(optionParams, pathNodes) - onPathChange?.(optionParams, pathNodes) - setInnerValue(optionParams) + const [visible, setVisible] = usePropsValue({ + value: outerVisible, + defaultValue: undefined, + onChange: (value) => { + if (value === false) { + props.onClose?.() } - setOptionsData(state.panes) - close() - return - } - // 如果有子节点,滑到下一个 - if (state.tree.hasChildren(node, isLazy())) { - const level = (node.level as number) + 1 - - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, level) - state.tabsCursor = level - state.panes.push({ - nodes: node.children || [], - selectedNode: null, - paneKey: `c${state.tabsCursor + 1}`, - }) - setOptionsData(state.panes) - setTabvalue(`c${state.tabsCursor + 1}`) + }, + }) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item?.value) - onPathChange?.(optionParams, pathNodes) - } - return - } - state.currentProcessNode = node - if (node.loading) { - return + useEffect(() => { + setTabActiveIndex(levels.length - 1) + }, [value]) + useEffect(() => { + const max = levels.length - 1 + if (tabActiveIndex > max) { + setTabActiveIndex(max) } + }, [tabActiveIndex, levels]) - await invokeLazyLoad(node) - if (state.currentProcessNode === node) { - state.panes[state.tabsCursor].selectedNode = node - chooseItem(node, type) + const chooseItem = (pane: CascaderOption, levelIndex: number) => { + if (pane.disabled) return + const nextValue = value.slice(0, levelIndex) + if (pane.value) { + nextValue[levelIndex] = pane.value + } + setValue(nextValue) + if (!pane.children) { + setVisible(false) } - setOptionsData(state.panes) } - const renderItem = (pane: any, node: any, index: number) => { - const classPrefix2 = 'nut-cascader-item' - const checked = pane.selectedNode?.value === node.value - - const classes = classNames( - { - active: checked, - disabled: node.disabled, - }, - classPrefix2 - ) - - const classesTitle = classNames({ - [`${classPrefix2}-title`]: true, + const renderCascaderItem = (item: any, levelIndex: number) => { + return item.pane.map((pane: CascaderOption, index: number) => { + const classes = classNames( + { + active: item.selected === pane.value, + disabled: pane.disabled, + }, + 'nut-cascader-item' + ) + return ( +
{ + chooseItem(pane, levelIndex) + }} + > +
{pane.text}
+ {/**/} + {item.selected === pane.value ? ( + + ) : null} +
+ ) }) - - const renderIcon = () => { - if (checked) { - if (isValidElement(activeIcon)) { - return activeIcon - } - return ( - - ) - } - return null - } - - return ( -
{ - chooseItem(node, false) - }} - > -
{node.text}
- {node.loading ? ( - - ) : ( - renderIcon() - )} -
- ) } - const renderTabs = () => { + const renderTab = () => { return ( -
+
{ - return optionsData.map((pane, index) => ( -
{ - setTabvalue(pane.paneKey) - state.tabsCursor = index - }} - className={`nut-tabs-titles-item ${ - tabvalue === pane.paneKey ? 'nut-tabs-titles-item-active' : '' - }`} - key={pane.paneKey} - > - - {!state.initLoading && - state.panes.length && - pane?.selectedNode?.text} - {!state.initLoading && - state.panes.length && - !pane?.selectedNode?.text && - `${locale.select}`} - {!(!state.initLoading && state.panes.length) && 'Loading...'} - - -
- )) + value={tabActiveIndex} + onChange={(index) => { + setTabActiveIndex(Number(index)) }} > - {!state.initLoading && state.panes.length ? ( - optionsData.map((pane) => ( - -
- {pane.nodes?.map((node: any, index: number) => - renderItem(pane, node, index) - )} -
-
- )) - ) : ( - -
+ {levels.map((pane, index) => ( + +
{renderCascaderItem(pane, index)}
- )} + ))}
) } - return ( - <> - {popup ? ( - - {renderTabs()} - - ) : ( - renderTabs() - )} - + return popup ? ( + { + setVisible(false) + }} + onCloseIconClick={() => { + setVisible(false) + }} + > + {renderTab()} + + ) : ( + renderTab() ) } -export const Cascader = React.forwardRef(InternalCascader) - Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/demo.tsx b/src/packages/cascader/demo.tsx index 484170b715..1fcaf2df92 100644 --- a/src/packages/cascader/demo.tsx +++ b/src/packages/cascader/demo.tsx @@ -39,17 +39,17 @@ const CascaderDemo = () => { <>

{translated.basic}

- + {/**/}

{translated.title1}

- + {/**/}

{translated.title2}

- + {/**/}

{translated.title3}

- + {/**/}

{translated.title4}

{translated.title5}

- + {/**/}
) diff --git a/src/packages/cascader/helper.ts b/src/packages/cascader/helper.ts index 2e1e14cf23..50274f965a 100644 --- a/src/packages/cascader/helper.ts +++ b/src/packages/cascader/helper.ts @@ -1,36 +1,4 @@ -import { CascaderOption, CascaderConfig, CascaderFormat } from './types' - -export const formatTree = ( - tree: CascaderOption[], - parent: CascaderOption | null, - config: CascaderConfig -): CascaderOption[] => - tree.map((node: any) => { - const { - value: valueKey = 'value', - text: textKey = 'text', - children: childrenKey = 'children', - } = config - const { - [valueKey]: value, - [textKey]: text, - [childrenKey]: children, - ...others - } = node - const newNode: CascaderOption = { - loading: false, - ...others, - level: parent ? ((parent && parent.level) || 0) + 1 : 0, - value, - text, - children, - _parent: parent, - } - if (newNode.children && newNode.children.length) { - newNode.children = formatTree(newNode.children, newNode, config) - } - return newNode - }) +import { CascaderOption, CascaderFormat, CascaderOptionKey } from './types' export const eachTree = ( tree: CascaderOption[], @@ -48,24 +16,24 @@ export const eachTree = ( } } -const defaultConvertConfig = { - topId: null, - idKey: 'id', - pidKey: 'pid', - sortKey: '', -} export const convertListToOptions = ( - list: CascaderOption[], - options: CascaderFormat + options: CascaderOption[], + format: CascaderFormat ): CascaderOption[] => { - const mergedOptions = { + const defaultConvertConfig = { + topId: null, + idKey: 'id', + pidKey: 'pid', + sortKey: '', + } + const mergedFormat = { ...defaultConvertConfig, - ...(options || {}), + ...format, } - const { topId, idKey, pidKey, sortKey } = mergedOptions + const { topId, idKey, pidKey, sortKey } = mergedFormat let result: CascaderOption[] = [] let map: any = {} - list.forEach((node: any) => { + options.forEach((node: any) => { node = { ...node } const { [idKey]: id, [pidKey]: pid } = node const children = (map[pid] = map[pid] || []) @@ -86,3 +54,24 @@ export const convertListToOptions = ( map = null return result } + +export const normalizeOptions = ( + options: CascaderOption[], + keyMap: CascaderOptionKey +): CascaderOption[] | undefined => { + if (!options) return undefined + return options.map((opt: any) => { + const { + [keyMap.textKey]: text, + [keyMap.valueKey]: value, + [keyMap.childrenKey]: children, + ...others + } = opt + return { + text, + value, + children: normalizeOptions(children, keyMap), + ...others, + } as CascaderOption + }) +} diff --git a/src/packages/cascader/tree.ts b/src/packages/cascader/tree.ts index a4e0ce24b2..cbfd95204a 100644 --- a/src/packages/cascader/tree.ts +++ b/src/packages/cascader/tree.ts @@ -78,6 +78,39 @@ class Tree { const { children } = node return Array.isArray(children) && Boolean(children.length) } + + static convert2Tree = ( + arr: any[], + format: Record + ) => { + const defaultConvertConfig = { + topId: null, + idKey: 'id', + pidKey: 'pid', + sortKey: '', + } + const { topId, idKey, pidKey, sortKey } = { + ...defaultConvertConfig, + ...format, + } + const idMap: { [key: string]: any } = {} + const tree = [] + arr.forEach((node) => { + idMap[pidKey] = { ...node } + }) + arr.forEach((item) => { + const currentNode = idMap[item[idKey]] + if (item[pidKey] === null) { + tree.push(currentNode) + } else { + // 非根节点,添加到父节点的 children 数组中 + const parentNode = idMap[item[pidKey]] + if (parentNode) { + parentNode.children.push(currentNode) + } + } + }) + } } export default Tree diff --git a/src/packages/cascader/types.ts b/src/packages/cascader/types.ts index 008a3d07c4..113d2c2981 100644 --- a/src/packages/cascader/types.ts +++ b/src/packages/cascader/types.ts @@ -36,3 +36,8 @@ export interface CascaderFormat { pidKey?: string sortKey?: string } + +export type CascaderActions = { + open: () => void + close: () => void +} diff --git a/src/packages/tabs/tabs.tsx b/src/packages/tabs/tabs.tsx index 103cbdb4a8..2ab488db1c 100644 --- a/src/packages/tabs/tabs.tsx +++ b/src/packages/tabs/tabs.tsx @@ -24,7 +24,7 @@ export interface TabsProps extends BasicComponent { activeType: 'line' | 'smile' | 'simple' | 'card' | 'button' | 'divider' duration: number | string align: 'left' | 'right' - title: () => JSX.Element[] + title: () => Element[] onChange: (index: string | number) => void onClick: (index: string | number) => void autoHeight: boolean diff --git a/src/utils/is-empty.ts b/src/utils/is-empty.ts new file mode 100644 index 0000000000..c4570c2773 --- /dev/null +++ b/src/utils/is-empty.ts @@ -0,0 +1,6 @@ +export const isEmpty = (p: T): boolean => { + if (p === null || p === undefined) return true + if (typeof p === 'object' && Object.keys(p).length === 0) return true + if (Array.isArray(p) && p.length === 0) return true + return false +} From 80778aeb7ed5d54c1227179c79852383462f3786 Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Wed, 22 Jan 2025 18:07:30 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E8=BF=98=E6=9C=AA=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=BC=82=E6=AD=A5=E5=8A=A0=E8=BD=BD=E7=9A=84=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cascader/__tests__/cascader.spec.tsx | 2 +- src/packages/cascader/cascader-origin.tsx | 2 +- src/packages/cascader/cascader.taro.tsx | 2 +- src/packages/cascader/cascader.tsx | 95 +++++++++++--- src/packages/cascader/demo.tsx | 10 +- src/packages/cascader/demos/h5/demo3.tsx | 26 ++-- src/packages/cascader/demos/h5/demo4.tsx | 24 ++-- src/packages/cascader/demos/h5/demo5.tsx | 7 +- src/packages/cascader/helper.ts | 77 ------------ src/packages/cascader/tree.ts | 116 ------------------ src/packages/cascader/utils.ts | 61 +++++++++ 11 files changed, 173 insertions(+), 249 deletions(-) delete mode 100644 src/packages/cascader/helper.ts delete mode 100644 src/packages/cascader/tree.ts create mode 100644 src/packages/cascader/utils.ts diff --git a/src/packages/cascader/__tests__/cascader.spec.tsx b/src/packages/cascader/__tests__/cascader.spec.tsx index 6299e5eaf8..4c03f33544 100644 --- a/src/packages/cascader/__tests__/cascader.spec.tsx +++ b/src/packages/cascader/__tests__/cascader.spec.tsx @@ -5,7 +5,7 @@ import { Cascader } from '../cascader' import { CascaderOption } from '../types' import Tree from '../tree' -import { formatTree, convertListToOptions } from '../helper' +import { formatTree, convertListToOptions } from '../utils' const later = (t = 0) => new Promise((r) => { diff --git a/src/packages/cascader/cascader-origin.tsx b/src/packages/cascader/cascader-origin.tsx index b3f80847dd..35f3ec317f 100644 --- a/src/packages/cascader/cascader-origin.tsx +++ b/src/packages/cascader/cascader-origin.tsx @@ -11,7 +11,7 @@ import classNames from 'classnames' import { Loading, Checklist } from '@nutui/icons-react' import { Popup } from '@/packages/popup/popup' import { Tabs } from '@/packages/tabs/tabs' -import { convertListToOptions } from './helper' +import { convertListToOptions } from './utils' import { CascaderPane, CascaderOption, diff --git a/src/packages/cascader/cascader.taro.tsx b/src/packages/cascader/cascader.taro.tsx index fb84efdbeb..f82ead61a8 100644 --- a/src/packages/cascader/cascader.taro.tsx +++ b/src/packages/cascader/cascader.taro.tsx @@ -12,7 +12,7 @@ import { Loading, Checklist } from '@nutui/icons-react-taro' import { ScrollView } from '@tarojs/components' import { Popup, PopupProps } from '@/packages/popup/popup.taro' import { Tabs } from '@/packages/tabs/tabs.taro' -import { convertListToOptions } from './helper' +import { convertListToOptions } from './utils' import { CascaderPane, CascaderOption, diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index 04e14e1496..b29d0a0f39 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -1,4 +1,11 @@ -import React, { ReactNode, useEffect, useMemo, useState } from 'react' +import React, { + isValidElement, + ReactNode, + useEffect, + useMemo, + useRef, + useState, +} from 'react' import { Checklist, Loading } from '@nutui/icons-react' import classNames from 'classnames' import Tabs from '@/packages/tabs' @@ -9,7 +16,10 @@ import { CascaderProps } from '@/packages/cascader/cascader-origin' import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' import { isEmpty } from '@/utils/is-empty' -import { convertListToOptions, normalizeOptions } from '@/packages/cascader/helper' +import { + normalizeListOptions, + normalizeOptions, +} from '@/packages/cascader/utils' export interface CascaderPorps extends PopupProps { visible: boolean @@ -21,9 +31,12 @@ export interface CascaderPorps extends PopupProps { closeable: boolean closeIcon: ReactNode closeIconPosition: string - onLoad: (node: CascaderValue) => Promise - onChange: (value: CascaderValue, params: any) => void - onPathChange: (value: CascaderValue, params: any) => void + onLoad: ( + node: CascaderOption, + levelIndex: number + ) => Promise + onChange: (value: CascaderValue, pathNodes: any) => void + onPathChange: (value: CascaderValue, pathNodes: any) => void onTabsChange: (index: number) => void onClose: () => void } @@ -40,7 +53,6 @@ const defaultProps = { closeIconPosition: 'top-right', closeIcon: 'close', lazy: false, - onLoad: () => {}, onClose: () => {}, onChange: () => {}, onPathChange: () => {}, @@ -69,9 +81,8 @@ export const Cascader = (props: Partial) => { const [tabActiveIndex, setTabActiveIndex] = useState(0) const options = useMemo(() => { - console.log(convertListToOptions(outerOptions, format)) if (!isEmpty(format)) { - return convertListToOptions(outerOptions, format) + return normalizeListOptions(outerOptions, format) } if (!isEmpty(optionKey)) { return normalizeOptions(outerOptions, optionKey) @@ -79,13 +90,15 @@ export const Cascader = (props: Partial) => { return outerOptions }, [outerOptions, optionKey, format]) + const pathNodes = useRef([]) + const [value, setValue] = usePropsValue({ value: outerValue, defaultValue: outerDefaultValue, finalValue: [], onChange: (value) => { - props.onChange?.(value, true) - props.onPathChange?.(value, true) + props.onChange?.(value, pathNodes.current) + props.onPathChange?.(value, pathNodes.current) }, }) @@ -99,6 +112,7 @@ export const Cascader = (props: Partial) => { selected: val, pane: currentOptions, }) + pathNodes.current.push(currentOptions) if (opt?.children) { currentOptions = opt.children } else { @@ -111,6 +125,7 @@ export const Cascader = (props: Partial) => { selected: null, pane: currentOptions, }) + pathNodes.current.push(currentOptions) } return next }, [value, options]) @@ -135,23 +150,64 @@ export const Cascader = (props: Partial) => { } }, [tabActiveIndex, levels]) - const chooseItem = (pane: CascaderOption, levelIndex: number) => { + useEffect(() => { + const load = async () => { + const parent = {} + try { + await value.reduce(async (promise: Promise, val, key) => { + const pane = await onLoad({ value: val }, key) + const parent = await promise + parent.children = pane + if (key === value.length - 1) { + return Promise.resolve(parent) + } + if (pane) { + const node = pane.find((p) => p.value === val) + return Promise.resolve(node) + } + }, Promise.resolve(parent)) + + // 如果需要处理最终结果,可以在这里使用 last + console.log('Final result:', parent) + options = parent.children + } catch (error) { + console.error('Error loading data:', error) + } + } + + if (lazy) load() + }, [lazy]) + + const chooseItem = async (pane: CascaderOption, levelIndex: number) => { if (pane.disabled) return const nextValue = value.slice(0, levelIndex) + const nextPathNodes = pathNodes.current.slice(0, levelIndex) if (pane.value) { nextValue[levelIndex] = pane.value } - setValue(nextValue) - if (!pane.children) { + pathNodes.current[levelIndex] = pane + if (onLoad) { + // 叶子节点不操作 + if (!pane.leaf) { + const asyncOptions = await onLoad(pane, levelIndex) + // 修改 options 触发渲染逻辑 + if (asyncOptions) pane.children = asyncOptions + } else { + setVisible(false) + } + } + if (!pane.children && !onLoad) { setVisible(false) } + setValue(nextValue) } const renderCascaderItem = (item: any, levelIndex: number) => { return item.pane.map((pane: CascaderOption, index: number) => { + const active = item.selected === pane.value const classes = classNames( { - active: item.selected === pane.value, + active, disabled: pane.disabled, }, 'nut-cascader-item' @@ -159,6 +215,7 @@ export const Cascader = (props: Partial) => { return (
{ chooseItem(pane, levelIndex) @@ -166,9 +223,12 @@ export const Cascader = (props: Partial) => { >
{pane.text}
{/**/} - {item.selected === pane.value ? ( - - ) : null} + {active && + (isValidElement(activeIcon) ? ( + activeIcon + ) : ( + + ))}
) }) @@ -180,6 +240,7 @@ export const Cascader = (props: Partial) => { { + props.onTabsChange?.(Number(index)) setTabActiveIndex(Number(index)) }} > diff --git a/src/packages/cascader/demo.tsx b/src/packages/cascader/demo.tsx index 1fcaf2df92..484170b715 100644 --- a/src/packages/cascader/demo.tsx +++ b/src/packages/cascader/demo.tsx @@ -39,17 +39,17 @@ const CascaderDemo = () => { <>

{translated.basic}

- {/**/} +

{translated.title1}

- {/**/} +

{translated.title2}

- {/**/} +

{translated.title3}

- {/**/} +

{translated.title4}

{translated.title5}

- {/**/} +
) diff --git a/src/packages/cascader/demos/h5/demo3.tsx b/src/packages/cascader/demos/h5/demo3.tsx index 48387206e4..2075e10a9f 100644 --- a/src/packages/cascader/demos/h5/demo3.tsx +++ b/src/packages/cascader/demos/h5/demo3.tsx @@ -3,29 +3,21 @@ import { Cascader, Cell } from '@nutui/nutui-react' const Demo3 = () => { const [isVisibleDemo3, setIsVisibleDemo3] = useState(false) - const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32']) + const [value3, setValue3] = useState(['A11', 'A21', 'A31', 'A41']) - const lazyLoadDemo3 = (node: any, resolve: (children: any) => void) => { - setTimeout(() => { - if (node.root) { - resolve([ - { value: 'A0', text: 'A0' }, - { value: 'B0', text: 'B0' }, - { value: 'C0', text: 'C0' }, - ]) - } else { - const { value, level } = node + const lazyLoadDemo3 = (node: any, level: number) => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node const text = value.substring(0, 1) const value1 = `${text}${level + 1}1` const value2 = `${text}${level + 1}2` - const value3 = `${text}${level + 1}3` resolve([ - { value: value1, text: value1, leaf: level >= 6 }, - { value: value2, text: value2, leaf: level >= 6 }, - { value: value3, text: value3, leaf: level >= 6 }, + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 2 }, ]) - } - }, 2000) + }, 500) + }) } const change3 = (value: any, path: any) => { console.log('onChange', value, path) diff --git a/src/packages/cascader/demos/h5/demo4.tsx b/src/packages/cascader/demos/h5/demo4.tsx index 1cfdce0e5e..4228a4377a 100644 --- a/src/packages/cascader/demos/h5/demo4.tsx +++ b/src/packages/cascader/demos/h5/demo4.tsx @@ -17,17 +17,19 @@ const Demo4 = () => { { value: 'C0', text: 'C0' }, ]) - const lazyLoadDemo4 = (node: any, resolve: (children: any) => void) => { - setTimeout(() => { - const { value, level } = node - const text = value.substring(0, 1) - const value1 = `${text}${level + 1}1` - const value2 = `${text}${level + 1}2` - resolve([ - { value: value1, text: value1, leaf: level >= 2 }, - { value: value2, text: value2, leaf: level >= 1 }, - ]) - }, 500) + const lazyLoadDemo4 = async (node: any, level: number) => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value.substring(0, 1) + const value1 = `${text}${level + 1}1` + const value2 = `${text}${level + 1}2` + resolve([ + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 1 }, + ]) + }, 500) + }) } const change4 = (value: any, path: any) => { console.log('onChange', value, path) diff --git a/src/packages/cascader/demos/h5/demo5.tsx b/src/packages/cascader/demos/h5/demo5.tsx index 92834f7fc1..0219eef26c 100644 --- a/src/packages/cascader/demos/h5/demo5.tsx +++ b/src/packages/cascader/demos/h5/demo5.tsx @@ -6,8 +6,10 @@ const Demo5 = () => { const [value5, setValue5] = useState(['广东省', '广州市']) const [optionsDemo5] = useState([ { value: '北京', text: '北京', id: 1, pidd: null }, - { value: '通州区', text: '通州区', id: 11, pidd: 1 }, - { value: '经海路', text: '经海路', id: 111, pidd: 11 }, + { value: '通州区', text: '通州区', id: 11, pidd: 1, sortKey: 2 }, + { value: '大兴区', text: '大兴区', id: 12, pidd: 1, sortKey: 1 }, + { value: '经海路', text: '经海路', id: 111, pidd: 12, sortKey: 2 }, + { value: '黄亦路', text: '黄亦路', id: 112, pidd: 12, sortKey: 1 }, { value: '广东省', text: '广东省', id: 2, pidd: null }, { value: '广州市', text: '广州市', id: 21, pidd: 2 }, ]) @@ -15,7 +17,6 @@ const Demo5 = () => { topId: null, idKey: 'id', pidKey: 'pidd', - sortKey: '', }) const change5 = (value: any, path: any) => { console.log('onChange', value, path) diff --git a/src/packages/cascader/helper.ts b/src/packages/cascader/helper.ts deleted file mode 100644 index 50274f965a..0000000000 --- a/src/packages/cascader/helper.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { CascaderOption, CascaderFormat, CascaderOptionKey } from './types' - -export const eachTree = ( - tree: CascaderOption[], - cb: (node: CascaderOption) => unknown -): void => { - let i = 0 - let node: CascaderOption - while ((node = tree[i++])) { - if (cb(node) === true) { - break - } - if (node.children && node.children.length) { - eachTree(node.children, cb) - } - } -} - -export const convertListToOptions = ( - options: CascaderOption[], - format: CascaderFormat -): CascaderOption[] => { - const defaultConvertConfig = { - topId: null, - idKey: 'id', - pidKey: 'pid', - sortKey: '', - } - const mergedFormat = { - ...defaultConvertConfig, - ...format, - } - const { topId, idKey, pidKey, sortKey } = mergedFormat - let result: CascaderOption[] = [] - let map: any = {} - options.forEach((node: any) => { - node = { ...node } - const { [idKey]: id, [pidKey]: pid } = node - const children = (map[pid] = map[pid] || []) - if (!result.length && pid === topId) { - result = children - } - children.push(node) - node.children = map[id] || (map[id] = []) - }) - - if (sortKey) { - Object.keys(map).forEach((i) => { - if (map[i].length > 1) { - map[i].sort((a: any, b: any) => a[sortKey] - b[sortKey]) - } - }) - } - map = null - return result -} - -export const normalizeOptions = ( - options: CascaderOption[], - keyMap: CascaderOptionKey -): CascaderOption[] | undefined => { - if (!options) return undefined - return options.map((opt: any) => { - const { - [keyMap.textKey]: text, - [keyMap.valueKey]: value, - [keyMap.childrenKey]: children, - ...others - } = opt - return { - text, - value, - children: normalizeOptions(children, keyMap), - ...others, - } as CascaderOption - }) -} diff --git a/src/packages/cascader/tree.ts b/src/packages/cascader/tree.ts deleted file mode 100644 index cbfd95204a..0000000000 --- a/src/packages/cascader/tree.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { CascaderOption, CascaderConfig, CascaderValue } from './types' -import { formatTree, eachTree } from './helper' - -class Tree { - nodes: CascaderOption[] - - readonly config: CascaderConfig - - constructor(nodes: CascaderOption[], config?: CascaderConfig) { - this.config = { - value: 'value', - text: 'text', - children: 'children', - ...(config || {}), - } - this.nodes = formatTree(nodes, null, this.config) - } - - updateChildren(nodes: CascaderOption[], parent: CascaderOption | null): void { - if (!parent) { - this.nodes = formatTree(nodes, null, this.config) - } else { - parent.children = formatTree(nodes, parent, this.config) - } - } - - // for test - getNodeByValue(value: CascaderOption['value']): CascaderOption | void { - let foundNode - eachTree(this.nodes, (node: CascaderOption) => { - if (node.value === value) { - foundNode = node - return true - } - return null - }) - return foundNode - } - - getPathNodesByValue(value: CascaderValue): CascaderOption[] { - if (!value.length) { - return [] - } - - const pathNodes = [] - let currentNodes: CascaderOption[] | void = this.nodes - - while (currentNodes && currentNodes.length) { - const foundNode: CascaderOption | void = currentNodes.find( - (node) => node.value === value[node.level as number] - ) - - if (!foundNode) { - break - } - - pathNodes.push(foundNode) - currentNodes = foundNode.children - } - - return pathNodes - } - - // eslint-disable-next-line class-methods-use-this - isLeaf = (node: CascaderOption, lazy: boolean): boolean => { - const { leaf, children } = node - const hasChildren = Array.isArray(children) && Boolean(children.length) - return leaf || (!hasChildren && !lazy) - } - - hasChildren = (node: CascaderOption, lazy: boolean): boolean => { - const isLeaf = this.isLeaf(node, lazy) - - if (isLeaf) { - return false - } - - const { children } = node - return Array.isArray(children) && Boolean(children.length) - } - - static convert2Tree = ( - arr: any[], - format: Record - ) => { - const defaultConvertConfig = { - topId: null, - idKey: 'id', - pidKey: 'pid', - sortKey: '', - } - const { topId, idKey, pidKey, sortKey } = { - ...defaultConvertConfig, - ...format, - } - const idMap: { [key: string]: any } = {} - const tree = [] - arr.forEach((node) => { - idMap[pidKey] = { ...node } - }) - arr.forEach((item) => { - const currentNode = idMap[item[idKey]] - if (item[pidKey] === null) { - tree.push(currentNode) - } else { - // 非根节点,添加到父节点的 children 数组中 - const parentNode = idMap[item[pidKey]] - if (parentNode) { - parentNode.children.push(currentNode) - } - } - }) - } -} - -export default Tree diff --git a/src/packages/cascader/utils.ts b/src/packages/cascader/utils.ts new file mode 100644 index 0000000000..872fdc7cb2 --- /dev/null +++ b/src/packages/cascader/utils.ts @@ -0,0 +1,61 @@ +import { CascaderOption, CascaderFormat, CascaderOptionKey } from './types' + +export const normalizeOptions = ( + options: CascaderOption[], + keyMap: CascaderOptionKey +): CascaderOption[] | undefined => { + if (!options) return undefined + return options.map((opt: any) => { + const { + [keyMap.textKey]: text, + [keyMap.valueKey]: value, + [keyMap.childrenKey]: children, + ...others + } = opt + return { + text, + value, + children: normalizeOptions(children, keyMap), + ...others, + } as CascaderOption + }) +} + +export const normalizeListOptions = ( + options: CascaderOption[], + format: CascaderFormat +) => { + const defaultConvertConfig = { + topId: null, + idKey: 'id', + pidKey: 'pid', + sortKey: 'sortKey', + } + const mergedFormat = { + ...defaultConvertConfig, + ...format, + } + const { topId, idKey, pidKey, sortKey } = mergedFormat + const map: { [key: string]: CascaderOption[] } = {} + options.forEach((opt) => { + const { [pidKey]: pid, [idKey]: id, ...others } = opt as any + const newNode: any = { pid, id, ...others } + if (map[pid]) { + map[pid].push(newNode) + } else { + map[pid] = [newNode] + } + }) + for (const key in map) { + // eslint-disable-next-line no-continue + if (!Object.prototype.hasOwnProperty.call(map, key)) continue + map[key].sort((a: any, b: any) => a[sortKey] - b[sortKey]) + map[key].forEach((option: any) => { + if (map[option.id]) { + option.children = map[option.id] + } + }) + } + // @ts-ignore + return map[topId] +} From 16abd07e7f18d59eae1ea046ec80ca2a4a080b26 Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 13:36:17 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20lazy=20load=EF=BC=8C=E6=9C=AA?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20loading=20=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/cascader.tsx | 31 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index b29d0a0f39..218a951f6e 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -20,6 +20,7 @@ import { normalizeListOptions, normalizeOptions, } from '@/packages/cascader/utils' +import { getRefValue, useRefState } from '@/utils/use-ref-state' export interface CascaderPorps extends PopupProps { visible: boolean @@ -80,15 +81,19 @@ export const Cascader = (props: Partial) => { const [tabActiveIndex, setTabActiveIndex] = useState(0) + const [optionsRef, setInnerOptions] = useRefState(outerOptions) + const innerOptions = getRefValue(optionsRef) + const options = useMemo(() => { + console.log('change inner options', innerOptions) if (!isEmpty(format)) { - return normalizeListOptions(outerOptions, format) + return normalizeListOptions(innerOptions, format) } if (!isEmpty(optionKey)) { - return normalizeOptions(outerOptions, optionKey) + return normalizeOptions(innerOptions, optionKey) } - return outerOptions - }, [outerOptions, optionKey, format]) + return innerOptions + }, [innerOptions, optionKey, format]) const pathNodes = useRef([]) @@ -106,13 +111,13 @@ export const Cascader = (props: Partial) => { const next = [] let end = false let currentOptions = options - for (const val of value) { + for (const [index, val] of value.entries()) { const opt = currentOptions.find((o) => o.value === val) next.push({ selected: val, pane: currentOptions, }) - pathNodes.current.push(currentOptions) + pathNodes.current[index] = opt if (opt?.children) { currentOptions = opt.children } else { @@ -125,10 +130,9 @@ export const Cascader = (props: Partial) => { selected: null, pane: currentOptions, }) - pathNodes.current.push(currentOptions) } return next - }, [value, options]) + }, [value, options, innerOptions]) const [visible, setVisible] = usePropsValue({ value: outerVisible, @@ -149,10 +153,9 @@ export const Cascader = (props: Partial) => { setTabActiveIndex(max) } }, [tabActiveIndex, levels]) - useEffect(() => { const load = async () => { - const parent = {} + const parent = { children: [] } try { await value.reduce(async (promise: Promise, val, key) => { const pane = await onLoad({ value: val }, key) @@ -168,8 +171,7 @@ export const Cascader = (props: Partial) => { }, Promise.resolve(parent)) // 如果需要处理最终结果,可以在这里使用 last - console.log('Final result:', parent) - options = parent.children + setInnerOptions(parent.children) } catch (error) { console.error('Error loading data:', error) } @@ -184,8 +186,9 @@ export const Cascader = (props: Partial) => { const nextPathNodes = pathNodes.current.slice(0, levelIndex) if (pane.value) { nextValue[levelIndex] = pane.value + nextPathNodes[levelIndex] = pane + pathNodes.current = nextPathNodes } - pathNodes.current[levelIndex] = pane if (onLoad) { // 叶子节点不操作 if (!pane.leaf) { @@ -222,7 +225,7 @@ export const Cascader = (props: Partial) => { }} >
{pane.text}
- {/**/} + {active && (isValidElement(activeIcon) ? ( activeIcon From 947f4fb4f4c14bbf42533340ee54f5b5f94fb146 Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 14:50:08 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=E9=99=A4=20loading=20=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=9C=AA=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/cascader-origin.tsx | 68 ++++++++--- src/packages/cascader/cascader.tsx | 87 ++++++-------- src/packages/cascader/demos/h5/demo1.tsx | 134 +++++++++++---------- src/packages/cascader/demos/h5/demo2.tsx | 132 ++++++++++---------- src/packages/cascader/demos/h5/demo3.tsx | 32 ++--- src/packages/cascader/demos/h5/demo4.tsx | 58 +++++---- src/packages/cascader/demos/h5/demo5.tsx | 59 +++++---- src/packages/cascader/demos/h5/demo6.tsx | 140 ++++++++++++---------- src/packages/cascader/types.ts | 54 ++++++++- 9 files changed, 434 insertions(+), 330 deletions(-) diff --git a/src/packages/cascader/cascader-origin.tsx b/src/packages/cascader/cascader-origin.tsx index 35f3ec317f..f6c4c2f647 100644 --- a/src/packages/cascader/cascader-origin.tsx +++ b/src/packages/cascader/cascader-origin.tsx @@ -9,24 +9,46 @@ import React, { } from 'react' import classNames from 'classnames' import { Loading, Checklist } from '@nutui/icons-react' -import { Popup } from '@/packages/popup/popup' +import { Popup, PopupProps } from '@/packages/popup/popup' import { Tabs } from '@/packages/tabs/tabs' -import { convertListToOptions } from './utils' +import { convertListToOptions } from './helper' import { CascaderPane, CascaderOption, CascaderValue, CascaderOptionKey, - CascaderFormat, CascaderActions, + CascaderFormat, } from './types' import Tree from './tree' import { ComponentDefaults } from '@/utils/typings' import { usePropsValue } from '@/utils/use-props-value' import { useConfig } from '@/packages/configprovider' -export interface CascaderProps { +export interface CascaderProps + extends Pick< + PopupProps, + | 'className' + | 'style' + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' + > { popup: boolean - visible: boolean + popupProps: Partial< + Omit< + PopupProps, + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' + > + > + visible: boolean // popup visible activeColor: string activeIcon: string options: CascaderOption[] @@ -43,6 +65,11 @@ export interface CascaderProps { onPathChange: (value: CascaderValue, params: any) => void } +export type CascaderActions = { + open: () => void + close: () => void +} + const defaultProps = { ...ComponentDefaults, activeColor: '', @@ -349,7 +376,7 @@ const InternalCascader: ForwardRefRenderFunction< } const renderItem = (pane: any, node: any, index: number) => { - const nutCascaderItem = 'nut-cascader-item' + const classPrefix2 = 'nut-cascader-item' const checked = pane.selectedNode?.value === node.value const classes = classNames( @@ -357,22 +384,25 @@ const InternalCascader: ForwardRefRenderFunction< active: checked, disabled: node.disabled, }, - nutCascaderItem + classPrefix2 ) const classesTitle = classNames({ - [`${nutCascaderItem}-title`]: true, + [`${classPrefix2}-title`]: true, }) const renderIcon = () => { - if (!checked) return null - return isValidElement(activeIcon) ? ( - activeIcon - ) : ( - - ) + if (checked) { + if (isValidElement(activeIcon)) { + return activeIcon + } + return ( + + ) + } + return null } return ( @@ -457,7 +487,7 @@ const InternalCascader: ForwardRefRenderFunction< closeIcon={closeIcon} closeable={closeable} closeIconPosition={closeIconPosition} - title={title} + title={popup && (title as ReactNode)} left={left} // todo 只关闭,不处理逻辑。和popup的逻辑不一致。关闭时需要增加是否要处理回调 onOverlayClick={closePopup} @@ -472,6 +502,6 @@ const InternalCascader: ForwardRefRenderFunction< ) } -export const CascaderOrigin = React.forwardRef(InternalCascader) +export const Cascader = React.forwardRef(InternalCascader) -CascaderOrigin.displayName = 'NutCascader' +Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index 218a951f6e..fa757e015e 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -1,7 +1,8 @@ import React, { + forwardRef, isValidElement, - ReactNode, useEffect, + useImperativeHandle, useMemo, useRef, useState, @@ -9,40 +10,19 @@ import React, { import { Checklist, Loading } from '@nutui/icons-react' import classNames from 'classnames' import Tabs from '@/packages/tabs' -import Popup, { PopupProps } from '@/packages/popup' -import { CascaderValue, CascaderOptionKey, CascaderOption } from './types' -import { ComponentDefaults } from '@/utils/typings' -import { CascaderProps } from '@/packages/cascader/cascader-origin' -import { mergeProps } from '@/utils/merge-props' -import { usePropsValue } from '@/utils/use-props-value' -import { isEmpty } from '@/utils/is-empty' +import Popup from '@/packages/popup' import { normalizeListOptions, normalizeOptions, } from '@/packages/cascader/utils' +import { CascaderOption, CascaderActions, CascaderProps } from './types' +import { ComponentDefaults } from '@/utils/typings' +import { mergeProps } from '@/utils/merge-props' +import { usePropsValue } from '@/utils/use-props-value' +import { isEmpty } from '@/utils/is-empty' import { getRefValue, useRefState } from '@/utils/use-ref-state' -export interface CascaderPorps extends PopupProps { - visible: boolean - value: CascaderValue - defaultValue: CascaderValue - options: CascaderOption[] - optionKey: CascaderOptionKey - format: Record - closeable: boolean - closeIcon: ReactNode - closeIconPosition: string - onLoad: ( - node: CascaderOption, - levelIndex: number - ) => Promise - onChange: (value: CascaderValue, pathNodes: any) => void - onPathChange: (value: CascaderValue, pathNodes: any) => void - onTabsChange: (index: number) => void - onClose: () => void -} - -const defaultProps = { +const defaultProps: CascaderProps = { ...ComponentDefaults, activeColor: '', activeIcon: 'checklist', @@ -59,7 +39,7 @@ const defaultProps = { onPathChange: () => {}, } as unknown as CascaderProps -export const Cascader = (props: Partial) => { +export const Cascader = forwardRef((props: Partial, ref) => { const classPrefix = 'nut-cascader' const classPane = `${classPrefix}-pane` const { @@ -80,12 +60,20 @@ export const Cascader = (props: Partial) => { } = mergeProps(defaultProps, props) const [tabActiveIndex, setTabActiveIndex] = useState(0) - const [optionsRef, setInnerOptions] = useRefState(outerOptions) const innerOptions = getRefValue(optionsRef) + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + props.onChange?.(value, pathNodes.current) + props.onPathChange?.(value, pathNodes.current) + }, + }) + const options = useMemo(() => { - console.log('change inner options', innerOptions) if (!isEmpty(format)) { return normalizeListOptions(innerOptions, format) } @@ -93,26 +81,16 @@ export const Cascader = (props: Partial) => { return normalizeOptions(innerOptions, optionKey) } return innerOptions - }, [innerOptions, optionKey, format]) + }, [innerOptions, optionKey, format, value]) const pathNodes = useRef([]) - const [value, setValue] = usePropsValue({ - value: outerValue, - defaultValue: outerDefaultValue, - finalValue: [], - onChange: (value) => { - props.onChange?.(value, pathNodes.current) - props.onPathChange?.(value, pathNodes.current) - }, - }) - const levels: any[] = useMemo(() => { const next = [] let end = false let currentOptions = options for (const [index, val] of value.entries()) { - const opt = currentOptions.find((o) => o.value === val) + const opt = currentOptions?.find((o: CascaderOption) => o.value === val) next.push({ selected: val, pane: currentOptions, @@ -143,16 +121,29 @@ export const Cascader = (props: Partial) => { } }, }) + const actions: CascaderActions = { + open: () => { + setVisible(true) + }, + close: () => { + setVisible(false) + }, + } + useImperativeHandle(ref, () => actions) + + useEffect(() => { + setInnerOptions(outerOptions) + }, [outerOptions]) useEffect(() => { setTabActiveIndex(levels.length - 1) - }, [value]) + }, [value, innerOptions, outerOptions]) useEffect(() => { const max = levels.length - 1 if (tabActiveIndex > max) { setTabActiveIndex(max) } - }, [tabActiveIndex, levels]) + }, [tabActiveIndex, levels, innerOptions, outerOptions]) useEffect(() => { const load = async () => { const parent = { children: [] } @@ -206,7 +197,7 @@ export const Cascader = (props: Partial) => { } const renderCascaderItem = (item: any, levelIndex: number) => { - return item.pane.map((pane: CascaderOption, index: number) => { + return item.pane?.map((pane: CascaderOption, index: number) => { const active = item.selected === pane.value const classes = classNames( { @@ -279,6 +270,6 @@ export const Cascader = (props: Partial) => { ) : ( renderTab() ) -} +}) Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/demos/h5/demo1.tsx b/src/packages/cascader/demos/h5/demo1.tsx index 6ad63821de..3ed0233098 100644 --- a/src/packages/cascader/demos/h5/demo1.tsx +++ b/src/packages/cascader/demos/h5/demo1.tsx @@ -1,98 +1,102 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' const Demo1 = () => { - const [isVisibleDemo1, setIsVisibleDemo1] = useState(false) - const [value1, setValue1] = useState([]) - const [optionsDemo1] = useState([ - { - value: '浙江', - text: '浙江', - children: [ - { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setOptions([ { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '芙蓉区', text: '芙蓉区' }, - { value: '岳麓区', text: '岳麓区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, ], }, { - value: '岳阳', - text: '岳阳', + value: '福建', + text: '福建', children: [ - { value: '岳阳楼区', text: '岳阳楼区' }, - { value: '云溪区', text: '云溪区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ - { - value: '福州', - text: '福州', - children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, - ], - }, - ], - }, - ]) - const change1 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue1(value) - } + ]) + }, 300) + }, []) return ( <> { - setIsVisibleDemo1(true) + setVisible(true) }} /> { - setIsVisibleDemo1(false) + setVisible(false) }} - onChange={change1} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/h5/demo2.tsx b/src/packages/cascader/demos/h5/demo2.tsx index 0b8059a5d8..9fd82558ab 100644 --- a/src/packages/cascader/demos/h5/demo2.tsx +++ b/src/packages/cascader/demos/h5/demo2.tsx @@ -1,91 +1,95 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react' const Demo2 = () => { - const [isVisibleDemo2, setIsVisibleDemo2] = useState(false) - const [value2, setValue2] = useState(['福建', '福州', '台江区']) - const [optionsDemo2] = useState([ - { - value1: '浙江', - text1: '浙江', - items: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['福建', '福州', '台江区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ { - value1: '杭州', - text1: '杭州', - disabled: true, - items: [ - { value1: '西湖区', text1: '西湖区', disabled: true }, - { value1: '余杭区', text1: '余杭区' }, - ], - }, - { - value1: '温州', - text1: '温州', + value1: '浙江', + text1: '浙江', items: [ - { value1: '鹿城区', text1: '鹿城区' }, - { value1: '瓯海区', text1: '瓯海区' }, + { + value1: '杭州', + text1: '杭州', + disabled: true, + items: [ + { value1: '西湖区', text1: '西湖区', disabled: true }, + { value1: '余杭区', text1: '余杭区' }, + ], + }, + { + value1: '温州', + text1: '温州', + items: [ + { value1: '鹿城区', text1: '鹿城区' }, + { value1: '瓯海区', text1: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value1: '湖南', - text1: '湖南', - disabled: true, - items: [ { - value1: '长沙', - text1: '长沙', + value1: '湖南', + text1: '湖南', disabled: true, items: [ - { value1: '芙蓉区', text1: '芙蓉区' }, - { value1: '岳麓区', text1: '岳麓区' }, - ], - }, - { - value1: '岳阳', - text1: '岳阳', - children: [ - { value1: '岳阳楼区', text1: '岳阳楼区' }, - { value1: '云溪区', text1: '云溪区' }, + { + value1: '长沙', + text1: '长沙', + disabled: true, + items: [ + { value1: '芙蓉区', text1: '芙蓉区' }, + { value1: '岳麓区', text1: '岳麓区' }, + ], + }, + { + value1: '岳阳', + text1: '岳阳', + children: [ + { value1: '岳阳楼区', text1: '岳阳楼区' }, + { value1: '云溪区', text1: '云溪区' }, + ], + }, ], }, - ], - }, - { - value1: '福建', - text1: '福建', - items: [ { - value1: '福州', - text1: '福州', + value1: '福建', + text1: '福建', items: [ - { value1: '鼓楼区', text1: '鼓楼区' }, - { value1: '台江区', text1: '台江区' }, + { + value1: '福州', + text1: '福州', + items: [ + { value1: '鼓楼区', text1: '鼓楼区' }, + { value1: '台江区', text1: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change2 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue2(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo2(true) + setVisible(true) }} /> { }} closeable onClose={() => { - setIsVisibleDemo2(false) + setVisible(false) }} - onChange={change2} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/h5/demo3.tsx b/src/packages/cascader/demos/h5/demo3.tsx index 2075e10a9f..72e052eafb 100644 --- a/src/packages/cascader/demos/h5/demo3.tsx +++ b/src/packages/cascader/demos/h5/demo3.tsx @@ -1,15 +1,18 @@ import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react' const Demo3 = () => { - const [isVisibleDemo3, setIsVisibleDemo3] = useState(false) - const [value3, setValue3] = useState(['A11', 'A21', 'A31', 'A41']) + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['A11', 'A21', 'A31', 'A41']) - const lazyLoadDemo3 = (node: any, level: number) => { + const loadCascaderItemData = ( + node: CascaderOption, + level: number + ): Promise => { return new Promise((resolve) => { setTimeout(() => { const { value } = node - const text = value.substring(0, 1) + const text = value?.toString().substring(0, 1) const value1 = `${text}${level + 1}1` const value2 = `${text}${level + 1}2` resolve([ @@ -19,31 +22,30 @@ const Demo3 = () => { }, 500) }) } - const change3 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue3(value) + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo3(true) + setVisible(true) }} /> { - setIsVisibleDemo3(false) + setVisible(false) }} - onChange={change3} + onChange={onChange} lazy - onLoad={lazyLoadDemo3} + onLoad={loadCascaderItemData} /> ) diff --git a/src/packages/cascader/demos/h5/demo4.tsx b/src/packages/cascader/demos/h5/demo4.tsx index 4228a4377a..d3a89f6eb5 100644 --- a/src/packages/cascader/demos/h5/demo4.tsx +++ b/src/packages/cascader/demos/h5/demo4.tsx @@ -1,23 +1,31 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' const Demo4 = () => { - const [isVisibleDemo4, setIsVisibleDemo4] = useState(false) - const [value4, setValue4] = useState([]) - const [optionsDemo4] = useState([ - { value: 'A0', text: 'A0' }, - { - value: 'B0', - text: 'B0', - children: [ - { value: 'B11', text: 'B11', leaf: true }, - { value: 'B12', text: 'B12' }, - ], - }, - { value: 'C0', text: 'C0' }, - ]) + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ + { value: 'A0', text: 'A0' }, + { + value: 'B0', + text: 'B0', + children: [ + { value: 'B11', text: 'B11', leaf: true }, + { value: 'B12', text: 'B12' }, + ], + }, + { value: 'C0', text: 'C0' }, + ]) + }, 300) + }, []) - const lazyLoadDemo4 = async (node: any, level: number) => { + const lazyLoadDemo4 = async ( + node: any, + level: number + ): Promise => { return new Promise((resolve) => { setTimeout(() => { const { value } = node @@ -32,30 +40,28 @@ const Demo4 = () => { }) } const change4 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue4(value) + setValue(value) } return ( <> { - setIsVisibleDemo4(true) + setVisible(true) }} /> { - setIsVisibleDemo4(false) + setVisible(false) }} onChange={change4} - lazy onLoad={lazyLoadDemo4} /> diff --git a/src/packages/cascader/demos/h5/demo5.tsx b/src/packages/cascader/demos/h5/demo5.tsx index 0219eef26c..12ae9501ec 100644 --- a/src/packages/cascader/demos/h5/demo5.tsx +++ b/src/packages/cascader/demos/h5/demo5.tsx @@ -1,48 +1,55 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react' const Demo5 = () => { - const [isVisibleDemo5, setIsVisibleDemo5] = useState(false) - const [value5, setValue5] = useState(['广东省', '广州市']) - const [optionsDemo5] = useState([ - { value: '北京', text: '北京', id: 1, pidd: null }, - { value: '通州区', text: '通州区', id: 11, pidd: 1, sortKey: 2 }, - { value: '大兴区', text: '大兴区', id: 12, pidd: 1, sortKey: 1 }, - { value: '经海路', text: '经海路', id: 111, pidd: 12, sortKey: 2 }, - { value: '黄亦路', text: '黄亦路', id: 112, pidd: 12, sortKey: 1 }, - { value: '广东省', text: '广东省', id: 2, pidd: null }, - { value: '广州市', text: '广州市', id: 21, pidd: 2 }, - ]) - const [convertConfigDemo5, setConvertConfigDemo5] = useState({ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const format = { topId: null, idKey: 'id', pidKey: 'pidd', - }) - const change5 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue5(value) + } + + useEffect(() => { + setTimeout(() => { + setValue(['广东省', '广州市']) + setOptions([ + { value: '北京', text: '北京', id: 1, pidd: null }, + { value: '通州区', text: '通州区', id: 11, pidd: 1, sortKey: 2 }, + { value: '大兴区', text: '大兴区', id: 12, pidd: 1, sortKey: 1 }, + { value: '经海路', text: '经海路', id: 111, pidd: 12, sortKey: 2 }, + { value: '黄亦路', text: '黄亦路', id: 112, pidd: 12, sortKey: 1 }, + { value: '广东省', text: '广东省', id: 2, pidd: null }, + { value: '广州市', text: '广州市', id: 21, pidd: 2 }, + ]) + }, 300) + }, []) + + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo5(true) + setVisible(true) }} /> { - setIsVisibleDemo5(false) + setVisible(false) }} - onChange={change5} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/h5/demo6.tsx b/src/packages/cascader/demos/h5/demo6.tsx index b7c21c3b5a..49efbea558 100644 --- a/src/packages/cascader/demos/h5/demo6.tsx +++ b/src/packages/cascader/demos/h5/demo6.tsx @@ -1,5 +1,10 @@ -import React, { useState } from 'react' -import { Cell, Cascader, ConfigProvider } from '@nutui/nutui-react' +import React, { useEffect, useState } from 'react' +import { + Cell, + Cascader, + ConfigProvider, + CascaderOption, +} from '@nutui/nutui-react' const customTheme = { nutuiCascaderItemHeight: '48px', @@ -11,74 +16,79 @@ const customTheme = { } const Demo6 = () => { - const [isVisibleDemo6, setIsVisibleDemo6] = useState(false) - const [value6, setValue6] = useState([]) - const [optionsDemo6] = useState([ - { - value: '浙江', - text: '浙江', - children: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['浙江', '温州', '鹿城区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + // setValue(['浙江', '温州', '鹿城区']) + setOptions([ { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '西湖区', text: '西湖区' }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', - children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区' }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ { - value: '福州', - text: '福州', + value: '福建', + text: '福建', children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change6 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue6(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } const onPathChange = (value: any, path: any) => { console.log('onPathChange', value, path) @@ -88,26 +98,26 @@ const Demo6 = () => { <> { - setIsVisibleDemo6(true) + setVisible(true) }} /> { - setIsVisibleDemo6(false) + setVisible(false) }} - onChange={change6} + onChange={onChange} onPathChange={onPathChange} - />{' '} + /> ) diff --git a/src/packages/cascader/types.ts b/src/packages/cascader/types.ts index 113d2c2981..de37a59bd0 100644 --- a/src/packages/cascader/types.ts +++ b/src/packages/cascader/types.ts @@ -1,3 +1,6 @@ +import { ReactNode } from 'react' +import { PopupProps } from '@/packages/popup' + export interface CascaderPane { nodes: [] selectedNode: CascaderOption | null @@ -11,9 +14,9 @@ export interface CascaderOption { disabled?: boolean children?: CascaderOption[] leaf?: boolean - level?: number loading?: boolean - root?: boolean + + [key: string]: any } export interface CascaderConfig { @@ -41,3 +44,50 @@ export type CascaderActions = { open: () => void close: () => void } +export type CascaderPopupProps = Pick< + PopupProps, + | 'className' + | 'style' + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' +> +export type CascaderSupportPopupProps = Partial< + Omit< + PopupProps, + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' + > +> + +export interface CascaderProps extends CascaderPopupProps { + visible: boolean + value: CascaderValue + activeColor: string + activeIcon: ReactNode + defaultValue: CascaderValue + options: CascaderOption[] + optionKey: CascaderOptionKey + format: Record + closeable: boolean + closeIcon: ReactNode + closeIconPosition: string + popup: boolean + popupProps: CascaderSupportPopupProps + lazy: boolean + onLoad: ( + node: CascaderOption, + levelIndex: number + ) => Promise + onChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onPathChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onTabsChange: (index: number) => void + onClose: () => void +} From dbfec245d49c1f086e7366f39dc7eac7ae9f5ddb Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 15:05:52 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=AF=8F?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E9=80=89=E9=A1=B9=E7=9A=84=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/cascader.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index fa757e015e..1108901dde 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -21,6 +21,7 @@ import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' import { isEmpty } from '@/utils/is-empty' import { getRefValue, useRefState } from '@/utils/use-ref-state' +import { useConfig } from '@/packages/configprovider' const defaultProps: CascaderProps = { ...ComponentDefaults, @@ -58,10 +59,12 @@ export const Cascader = forwardRef((props: Partial, ref) => { lazy, onLoad, } = mergeProps(defaultProps, props) + const { locale } = useConfig() const [tabActiveIndex, setTabActiveIndex] = useState(0) const [optionsRef, setInnerOptions] = useRefState(outerOptions) const innerOptions = getRefValue(optionsRef) + const [loading, setLoading] = useState<{ [key: string]: any }>({}) const [value, setValue] = usePropsValue({ value: outerValue, @@ -176,6 +179,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { const nextValue = value.slice(0, levelIndex) const nextPathNodes = pathNodes.current.slice(0, levelIndex) if (pane.value) { + setLoading(!!onLoad && { [levelIndex]: pane.value }) nextValue[levelIndex] = pane.value nextPathNodes[levelIndex] = pane pathNodes.current = nextPathNodes @@ -194,6 +198,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { setVisible(false) } setValue(nextValue) + setLoading({}) } const renderCascaderItem = (item: any, levelIndex: number) => { @@ -206,6 +211,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { }, 'nut-cascader-item' ) + const showLoadingIcon = loading[levelIndex] === pane.value return (
, ref) => { }} >
{pane.text}
- + {showLoadingIcon && ( + + )} {active && (isValidElement(activeIcon) ? ( activeIcon @@ -239,7 +250,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { }} > {levels.map((pane, index) => ( - +
{renderCascaderItem(pane, index)}
))} From 104ef70454516f51af5fd42e88c26c02431c0565 Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 15:37:09 +0800 Subject: [PATCH 06/11] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__snapshots__/cascader.spec.tsx.snap | 44 ++-- .../cascader/__tests__/cascader.spec.tsx | 190 ++---------------- src/packages/cascader/cascader.tsx | 12 +- 3 files changed, 41 insertions(+), 205 deletions(-) diff --git a/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap b/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap index db50b5b19b..21d7de3247 100644 --- a/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap +++ b/src/packages/cascader/__tests__/__snapshots__/cascader.spec.tsx.snap @@ -19,7 +19,7 @@ exports[`Cascader > visible true 1`] = ` style="z-index: 1000;" >
visible true 1`] = ` class="nut-tabs-titles nut-tabs-titles-line nut-tabs-titles-scrollable" >
- +
福建 - - +
- +
福州 - - +
- +
鼓楼区 - - +
visible true 1`] = `
- new Promise((r) => { - setTimeout(r, t) - }) const mockOptions = [ { value: '浙江', @@ -92,163 +85,6 @@ const mockConvertOptions = [ { value: '广州市', text: '广州市', nodeId: 21, nodePid: 2 }, ] -describe('helpers', () => { - test('formatTree', () => { - const fromatedTree = formatTree(mockKeyConfigOptions, null, { - children: 'items', - text: 'name', - value: 'name', - }) - - expect(fromatedTree).toMatchObject(mockOptions) - }) - - test('convertListToOptions', () => { - const convertList = convertListToOptions(mockConvertOptions, { - topId: 0, - idKey: 'nodeId', - pidKey: 'nodePid', - sortKey: '', - }) - expect(convertList).toMatchObject([ - { - nodePid: 0, - nodeId: 1, - text: '北京', - value: '北京', - sort: 2, - children: [ - { - nodePid: 1, - nodeId: 11, - text: '朝阳区', - value: '朝阳区', - children: [ - { - nodePid: 11, - nodeId: 111, - text: '亦庄', - value: '亦庄', - children: [], - }, - ], - }, - ], - }, - { - nodePid: 0, - nodeId: 2, - text: '广东省', - value: '广东省', - children: [ - { - nodePid: 2, - nodeId: 21, - text: '广州市', - value: '广州市', - }, - ], - }, - ]) - }) -}) - -describe('Tree', () => { - test('tree', () => { - const tree = new Tree([ - { - text: '浙江', - value: '浙江', - }, - { - text: '福建', - value: '福建', - }, - ]) - expect(tree.nodes).toMatchObject([ - { - text: '浙江', - value: '浙江', - }, - { - text: '福建', - value: '福建', - }, - ]) - }) - - test('tree with config', () => { - const tree = new Tree(mockKeyConfigOptions, { - value: 'name', - text: 'name', - children: 'items', - }) - expect(tree.nodes).toMatchObject(mockOptions) - }) - - const tree = new Tree(mockOptions) - test('getPathNodesByValue', () => { - const pathNodes = tree.getPathNodesByValue(['浙江', '杭州', '西湖区']) - const mappedPathNodes = pathNodes.map(({ text, value }) => ({ - text, - value, - })) - - expect(mappedPathNodes).toMatchObject([ - { text: '浙江', value: '浙江' }, - { text: '杭州', value: '杭州' }, - { text: '西湖区', value: '西湖区' }, - ]) - }) - - test('isLeaf', () => { - const node = tree.getNodeByValue('西湖区') - let isLeaf = tree.isLeaf(node as CascaderOption, false) - expect(isLeaf).toBeTruthy() - isLeaf = tree.isLeaf(node as CascaderOption, true) - expect(isLeaf).toBeFalsy() - }) - - test('hasChildren', () => { - let node = tree.getNodeByValue('西湖区') - - let hasChildren = tree.hasChildren(node as CascaderOption, false) - expect(hasChildren).toBeFalsy() - - hasChildren = tree.hasChildren(node as CascaderOption, true) - expect(hasChildren).toBeFalsy() - - node = tree.getNodeByValue('杭州') - - hasChildren = tree.hasChildren(node as CascaderOption, false) - expect(hasChildren).toBeTruthy() - - hasChildren = tree.hasChildren(node as CascaderOption, true) - expect(hasChildren).toBeTruthy() - }) - - test('updateChildren', () => { - let node = tree.getNodeByValue('福建') - expect(node).toBeTruthy() - - tree.updateChildren( - [{ text: '福州', value: '福州' }], - node as CascaderOption - ) - node = tree.getNodeByValue('福州') as CascaderOption - expect(node).toBeTruthy() - expect(node.value).toBe('福州') - - tree.updateChildren( - [{ text: '鼓楼区', value: '鼓楼区' }], - node as CascaderOption - ) - node = tree.getNodeByValue('鼓楼区') as CascaderOption - expect(node).toBeTruthy() - expect(node.value).toBe('鼓楼区') - }) -}) - describe('Cascader', () => { it('options', async () => { const { container } = render( @@ -330,11 +166,11 @@ describe('Cascader', () => { /> ) const element = container.querySelectorAll( - '.active.nut-tabpane .active .nut-cascader-item-title' + '.nut-tabs-titles-item-active .nut-tabs-titles-item-text' )[0] expect(element).toHaveTextContent('鼓楼区') }) - it('init Value with both valu and defaultValue', async () => { + it('init Value with both value and defaultValue', async () => { const { container } = render( { /> ) const element = container.querySelectorAll( - '.active.nut-tabpane .active .nut-cascader-item-title' + '.nut-tabs-titles-item-active .nut-tabs-titles-item-text' )[0] expect(element).toHaveTextContent('台江区') }) @@ -375,17 +211,19 @@ describe('Cascader', () => { const { container } = render( void) => { - setTimeout(() => { - lazyFunc() - resolve({}) - }, 50) + value={['test']} + onLoad={async (node: any) => { + return new Promise((resolve) => { + setTimeout(() => { + lazyFunc() + resolve([] as CascaderOption[]) + }, 50) + }) }} /> ) expect(lazyFunc).not.toBeCalled() - await act(async () => { - await later(100) + await waitFor(() => { expect(lazyFunc).toBeCalled() }) }) diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index 1108901dde..c61be2fdf6 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -47,6 +47,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { activeColor, activeIcon, popup, + popupProps = {}, visible: outerVisible, options: outerOptions, value: outerValue, @@ -261,6 +262,8 @@ export const Cascader = forwardRef((props: Partial, ref) => { return popup ? ( , ref) => { closeIconPosition={closeIconPosition} title={props.title} left={props.left} - visible={visible} - onOverlayClick={() => { - setVisible(false) - }} - onCloseIconClick={() => { - setVisible(false) - }} + onOverlayClick={() => setVisible(false)} + onCloseIconClick={() => setVisible(false)} > {renderTab()} From 085891ea7b7f2fcb0a578793441ca8e90c5e2a7f Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 15:53:12 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20taro=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/cascader.taro.tsx | 651 ++++++++------------- src/packages/cascader/cascader.tsx | 58 +- src/packages/cascader/demos/taro/demo1.tsx | 135 ++--- src/packages/cascader/demos/taro/demo2.tsx | 132 +++-- src/packages/cascader/demos/taro/demo3.tsx | 55 +- src/packages/cascader/demos/taro/demo4.tsx | 78 +-- src/packages/cascader/demos/taro/demo5.tsx | 58 +- src/packages/cascader/demos/taro/demo6.tsx | 140 +++-- src/packages/cascader/types.ts | 50 -- 9 files changed, 609 insertions(+), 748 deletions(-) diff --git a/src/packages/cascader/cascader.taro.tsx b/src/packages/cascader/cascader.taro.tsx index f82ead61a8..320b81845e 100644 --- a/src/packages/cascader/cascader.taro.tsx +++ b/src/packages/cascader/cascader.taro.tsx @@ -1,509 +1,338 @@ import React, { - ForwardRefRenderFunction, - PropsWithChildren, + forwardRef, isValidElement, - useState, - useEffect, ReactNode, + useEffect, useImperativeHandle, + useMemo, + useRef, + useState, } from 'react' +import { Checklist, Loading } from '@nutui/icons-react-taro' import classNames from 'classnames' -import { Loading, Checklist } from '@nutui/icons-react-taro' -import { ScrollView } from '@tarojs/components' -import { Popup, PopupProps } from '@/packages/popup/popup.taro' -import { Tabs } from '@/packages/tabs/tabs.taro' -import { convertListToOptions } from './utils' +import Tabs from '@/packages/tabs/index.taro' +import Popup, { PopupProps } from '@/packages/popup/index.taro' +import { useConfig } from '@/packages/configprovider/index.taro' +import { + normalizeListOptions, + normalizeOptions, +} from '@/packages/cascader/utils' import { - CascaderPane, CascaderOption, + CascaderActions, CascaderValue, CascaderOptionKey, - CascaderFormat, } from './types' -import Tree from './tree' import { ComponentDefaults } from '@/utils/typings' +import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' -import { useConfig } from '@/packages/configprovider/configprovider.taro' - -export interface CascaderProps - extends Pick< +import { isEmpty } from '@/utils/is-empty' +import { getRefValue, useRefState } from '@/utils/use-ref-state' + +export type CascaderPopupProps = Pick< + PopupProps, + | 'className' + | 'style' + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' +> +export type CascaderSupportPopupProps = Partial< + Omit< PopupProps, - | 'className' - | 'style' | 'closeIcon' | 'closeable' | 'title' | 'left' | 'closeIconPosition' | 'onClose' - > { - popup: boolean - popupProps: Partial< - Omit< - PopupProps, - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > > - visible: boolean // popup visible +> + +export interface CascaderProps extends CascaderPopupProps { + visible: boolean + value: CascaderValue activeColor: string - activeIcon: string + activeIcon: ReactNode + defaultValue: CascaderValue options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue optionKey: CascaderOptionKey format: Record closeable: boolean - closeIconPosition: string closeIcon: ReactNode + closeIconPosition: string + popup: boolean + popupProps: CascaderSupportPopupProps lazy: boolean - onLoad: (node: any, resolve: any) => void - onChange: (value: CascaderValue, params?: any) => void - onPathChange: (value: CascaderValue, params: any) => void -} - -export type CascaderActions = { - open: () => void - close: () => void + onLoad: ( + node: CascaderOption, + levelIndex: number + ) => Promise + onChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onPathChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onTabsChange: (index: number) => void + onClose: () => void } -const defaultProps = { +const defaultProps: CascaderProps = { ...ComponentDefaults, activeColor: '', activeIcon: 'checklist', popup: true, options: [], - optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + optionKey: {}, format: {}, closeable: false, closeIconPosition: 'top-right', closeIcon: 'close', lazy: false, - onLoad: () => {}, onClose: () => {}, onChange: () => {}, onPathChange: () => {}, } as unknown as CascaderProps -const InternalCascader: ForwardRefRenderFunction< - unknown, - PropsWithChildren> -> = (props, ref) => { - const { locale } = useConfig() + +export const Cascader = forwardRef((props: Partial, ref) => { + const classPrefix = 'nut-cascader' + const classPane = `${classPrefix}-pane` const { - className, - style, activeColor, activeIcon, popup, popupProps = {}, - visible, - options, - value, - defaultValue, + visible: outerVisible, + options: outerOptions, + value: outerValue, + defaultValue: outerDefaultValue, optionKey, format, closeable, closeIconPosition, closeIcon, lazy, - title, - left, onLoad, - onClose, - onChange, - onPathChange, - } = { ...defaultProps, ...props } - - const [tabvalue, setTabvalue] = useState('c1') - const [optionsData, setOptionsData] = useState([]) - const isLazy = () => state.configs.lazy && Boolean(state.configs.onLoad) + } = mergeProps(defaultProps, props) + const { locale } = useConfig() - const [innerValue, setInnerValue] = usePropsValue({ - value, - defaultValue, - finalValue: defaultValue, + const [tabActiveIndex, setTabActiveIndex] = useState(0) + const [optionsRef, setInnerOptions] = useRefState(outerOptions) + const innerOptions = getRefValue(optionsRef) + const [loading, setLoading] = useState<{ [key: string]: any }>({}) + + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + props.onChange?.(value, pathNodes.current) + props.onPathChange?.(value, pathNodes.current) + }, }) - const [innerVisible, setInnerVisible] = usePropsValue({ - value: visible, + + const options = useMemo(() => { + if (!isEmpty(format)) { + return normalizeListOptions(innerOptions, format) + } + if (!isEmpty(optionKey)) { + return normalizeOptions(innerOptions, optionKey) + } + return innerOptions + }, [innerOptions, optionKey, format, value]) + + const pathNodes = useRef([]) + + const levels: any[] = useMemo(() => { + const next = [] + let end = false + let currentOptions = options + for (const [index, val] of value.entries()) { + const opt = currentOptions?.find((o: CascaderOption) => o.value === val) + next.push({ + selected: val, + pane: currentOptions, + }) + pathNodes.current[index] = opt + if (opt?.children) { + currentOptions = opt.children + } else { + end = true + break + } + } + if (!end) { + next.push({ + selected: null, + pane: currentOptions, + }) + } + return next + }, [value, options, innerOptions]) + + const [visible, setVisible] = usePropsValue({ + value: outerVisible, defaultValue: undefined, - finalValue: false, + onChange: (value) => { + if (value === false) { + props.onClose?.() + } + }, }) const actions: CascaderActions = { open: () => { - setInnerVisible(true) + setVisible(true) }, close: () => { - setInnerVisible(false) + setVisible(false) }, } useImperativeHandle(ref, () => actions) - const [state] = useState({ - optionsData: [] as any, - panes: [ - { - nodes: [] as any, - selectedNode: [] as CascaderOption | null, - paneKey: '', - }, - ], - tree: new Tree([], {}), - tabsCursor: 0, // 选中的tab项 - initLoading: false, - currentProcessNode: [] as CascaderOption | null, - configs: { - lazy, - onLoad, - optionKey, - format, - }, - lazyLoadMap: new Map(), - }) - - const classPrefix = classNames(`nut-cascader`) - const classesPane = classNames({ - [`${classPrefix}-pane`]: true, - }) - useEffect(() => { - initData() - }, [options, format]) + setInnerOptions(outerOptions) + }, [outerOptions]) useEffect(() => { - syncValue() - }, [value]) - - const initData = async () => { - // 初始化开始处理数据 - state.lazyLoadMap.clear() - if (format && Object.keys(format).length > 0) { - state.optionsData = convertListToOptions( - options as CascaderOption[], - format as CascaderFormat - ) - } else { - state.optionsData = options - } - state.tree = new Tree(state.optionsData as CascaderOption[], { - value: state.configs.optionKey.valueKey, - text: state.configs.optionKey.textKey, - children: state.configs.optionKey.childrenKey, - }) - if (isLazy() && !state.tree.nodes.length) { - await invokeLazyLoad({ - root: true, - loading: true, - text: '', - value: '', - }) - } - state.panes = [ - { - nodes: state.tree.nodes, - selectedNode: null, - paneKey: 'c1', - }, - ] - syncValue() - setOptionsData(state.panes) - } - // 处理有默认值时的数据 - const syncValue = async () => { - const currentValue = innerValue - - if ( - currentValue === undefined || - ![defaultValue, value].includes(currentValue) || - !state.tree.nodes.length - ) { - return - } - - if (currentValue.length === 0) { - state.tabsCursor = 0 - return + setTabActiveIndex(levels.length - 1) + }, [value, innerOptions, outerOptions]) + useEffect(() => { + const max = levels.length - 1 + if (tabActiveIndex > max) { + setTabActiveIndex(max) } - - let needToSync = currentValue - - if (isLazy() && Array.isArray(currentValue) && currentValue.length) { - needToSync = [] - const parent: any = state.tree.nodes.find( - (node) => node.value === currentValue[0] - ) - - if (parent) { - needToSync = [parent.value] - state.initLoading = true - - const last = await currentValue - .slice(1) - .reduce(async (p: Promise, value) => { - const parent = await p - await invokeLazyLoad(parent) - const node: any = parent?.children?.find( - (item: any) => item.value === value - ) - if (node) { - needToSync.push(value) - } + }, [tabActiveIndex, levels, innerOptions, outerOptions]) + useEffect(() => { + const load = async () => { + const parent = { children: [] } + try { + await value.reduce(async (promise: Promise, val, key) => { + const pane = await onLoad({ value: val }, key) + const parent = await promise + parent.children = pane + if (key === value.length - 1) { + return Promise.resolve(parent) + } + if (pane) { + const node = pane.find((p) => p.value === val) return Promise.resolve(node) - }, Promise.resolve(parent)) - await invokeLazyLoad(last) - state.initLoading = false - } - } - - if (needToSync.length && [defaultValue, value].includes(currentValue)) { - const pathNodes = state.tree.getPathNodesByValue(needToSync) - pathNodes.forEach((node, index) => { - state.tabsCursor = index - // 当有默认值时,不触发 chooseItem 里的 emit 事件 - chooseItem(node, true) - }) - } - } - - const invokeLazyLoad = async (node?: CascaderOption | void) => { - if (!node) { - return - } - - if (!state.configs.onLoad) { - node.leaf = true - return - } - - if ( - state.tree.isLeaf(node, isLazy()) || - state.tree.hasChildren(node, isLazy()) - ) { - return - } - - node.loading = true + } + }, Promise.resolve(parent)) - const parent = node.root ? null : node - let lazyLoadPromise = state.lazyLoadMap.get(node) - - if (!lazyLoadPromise) { - lazyLoadPromise = new Promise((resolve) => { - // 外部必须resolve - state.configs.onLoad?.(node, resolve) - }) - state.lazyLoadMap.set(node, lazyLoadPromise) - } - - const nodes: CascaderOption[] | void = await lazyLoadPromise - - if (Array.isArray(nodes) && nodes.length > 0) { - state.tree.updateChildren(nodes, parent) - } else { - // 如果加载完成后没有提供子节点,作为叶子节点处理 - node.leaf = true - } - node.loading = false - state.lazyLoadMap.delete(node) - } - - const close = () => { - setInnerVisible(false) - onClose && onClose() - } - - const closePopup = () => { - close() - } - - /* type: 是否是静默模式,是的话不触发事件 - tabsCursor: tab的索引 */ - const chooseItem = async (node: CascaderOption, type: boolean) => { - if ((!type && node.disabled) || !state.panes[state.tabsCursor]) { - return - } - // 如果没有子节点 - if (state.tree.isLeaf(node, isLazy())) { - node.leaf = true - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, (node.level as number) + 1) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item.value) - onChange(optionParams, pathNodes) - onPathChange?.(optionParams, pathNodes) - setInnerValue(optionParams) + // 如果需要处理最终结果,可以在这里使用 last + setInnerOptions(parent.children) + } catch (error) { + console.error('Error loading data:', error) } - setOptionsData(state.panes) - close() - return } - // 如果有子节点,滑到下一个 - if (state.tree.hasChildren(node, isLazy())) { - const level = (node.level as number) + 1 - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, level) - state.tabsCursor = level - state.panes.push({ - nodes: node.children || [], - selectedNode: null, - paneKey: `c${state.tabsCursor + 1}`, - }) - setOptionsData(state.panes) - setTabvalue(`c${state.tabsCursor + 1}`) - - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item?.value) - onPathChange?.(optionParams, pathNodes) + if (lazy) load() + }, [lazy]) + + const chooseItem = async (pane: CascaderOption, levelIndex: number) => { + if (pane.disabled) return + const nextValue = value.slice(0, levelIndex) + const nextPathNodes = pathNodes.current.slice(0, levelIndex) + if (pane.value) { + setLoading(!!onLoad && { [levelIndex]: pane.value }) + nextValue[levelIndex] = pane.value + nextPathNodes[levelIndex] = pane + pathNodes.current = nextPathNodes + } + if (onLoad) { + // 叶子节点不操作 + if (!pane.leaf) { + const asyncOptions = await onLoad(pane, levelIndex) + // 修改 options 触发渲染逻辑 + if (asyncOptions) pane.children = asyncOptions + } else { + setVisible(false) } - return } - state.currentProcessNode = node - if (node.loading) { - return + if (!pane.children && !onLoad) { + setVisible(false) } - - await invokeLazyLoad(node) - if (state.currentProcessNode === node) { - state.panes[state.tabsCursor].selectedNode = node - chooseItem(node, type) - } - setOptionsData(state.panes) + setValue(nextValue) + setLoading({}) } - const renderItem = (pane: any, node: any, index: number) => { - const classPrefix2 = 'nut-cascader-item' - const checked = pane.selectedNode?.value === node.value - - const classes = classNames( - { - active: checked, - disabled: node.disabled, - }, - classPrefix2 - ) - - const classesTitle = classNames({ - [`${classPrefix2}-title`]: true, + const renderCascaderItem = (item: any, levelIndex: number) => { + return item.pane?.map((pane: CascaderOption, index: number) => { + const active = item.selected === pane.value + const classes = classNames( + { + active, + disabled: pane.disabled, + }, + 'nut-cascader-item' + ) + const showLoadingIcon = loading[levelIndex] === pane.value + return ( +
{ + chooseItem(pane, levelIndex) + }} + > +
{pane.text}
+ {showLoadingIcon && ( + + )} + {active && + (isValidElement(activeIcon) ? ( + activeIcon + ) : ( + + ))} +
+ ) }) - - const renderIcon = () => { - if (checked) { - if (isValidElement(activeIcon)) { - return activeIcon - } - return ( - - ) - } - return null - } - - return ( -
{ - chooseItem(node, false) - }} - > -
{node.text}
- {node.loading ? ( - - ) : ( - renderIcon() - )} -
- ) } - const renderTabs = () => { + const renderTab = () => { return ( -
+
{ - return optionsData.map((pane, index) => ( -
{ - setTabvalue(pane.paneKey) - state.tabsCursor = index - }} - className={`nut-tabs-titles-item ${ - tabvalue === pane.paneKey ? 'nut-tabs-titles-item-active' : '' - }`} - key={pane.paneKey} - > - - {!state.initLoading && - state.panes.length && - pane?.selectedNode?.text} - {!state.initLoading && - state.panes.length && - !pane?.selectedNode?.text && - `${locale.select}`} - {!(!state.initLoading && state.panes.length) && 'Loading...'} - - -
- )) + value={tabActiveIndex} + onChange={(index) => { + props.onTabsChange?.(Number(index)) + setTabActiveIndex(Number(index)) }} > - {!state.initLoading && state.panes.length ? ( - optionsData.map((pane) => ( - - - {pane.nodes?.map((node: any, index: number) => - renderItem(pane, node, index) - )} - - - )) - ) : ( - -
+ {levels.map((pane, index) => ( + +
{renderCascaderItem(pane, index)}
- )} + ))}
) } - return ( - <> - {popup ? ( - - {renderTabs()} - - ) : ( - renderTabs() - )} - + return popup ? ( + setVisible(false)} + onCloseIconClick={() => setVisible(false)} + > + {renderTab()} + + ) : ( + renderTab() ) -} - -export const Cascader = React.forwardRef(InternalCascader) +}) Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index c61be2fdf6..09da209648 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, isValidElement, + ReactNode, useEffect, useImperativeHandle, useMemo, @@ -10,12 +11,17 @@ import React, { import { Checklist, Loading } from '@nutui/icons-react' import classNames from 'classnames' import Tabs from '@/packages/tabs' -import Popup from '@/packages/popup' +import Popup, { PopupProps } from '@/packages/popup' import { normalizeListOptions, normalizeOptions, } from '@/packages/cascader/utils' -import { CascaderOption, CascaderActions, CascaderProps } from './types' +import { + CascaderOption, + CascaderActions, + CascaderValue, + CascaderOptionKey, +} from './types' import { ComponentDefaults } from '@/utils/typings' import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' @@ -23,6 +29,54 @@ import { isEmpty } from '@/utils/is-empty' import { getRefValue, useRefState } from '@/utils/use-ref-state' import { useConfig } from '@/packages/configprovider' +export type CascaderPopupProps = Pick< + PopupProps, + | 'className' + | 'style' + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' +> +export type CascaderSupportPopupProps = Partial< + Omit< + PopupProps, + | 'closeIcon' + | 'closeable' + | 'title' + | 'left' + | 'closeIconPosition' + | 'onClose' + > +> + +export interface CascaderProps extends CascaderPopupProps { + visible: boolean + value: CascaderValue + activeColor: string + activeIcon: ReactNode + defaultValue: CascaderValue + options: CascaderOption[] + optionKey: CascaderOptionKey + format: Record + closeable: boolean + closeIcon: ReactNode + closeIconPosition: string + popup: boolean + popupProps: CascaderSupportPopupProps + lazy: boolean + onLoad: ( + node: CascaderOption, + levelIndex: number + ) => Promise + onChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onPathChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void + onTabsChange: (index: number) => void + onClose: () => void +} + const defaultProps: CascaderProps = { ...ComponentDefaults, activeColor: '', diff --git a/src/packages/cascader/demos/taro/demo1.tsx b/src/packages/cascader/demos/taro/demo1.tsx index b8dbadeafd..3f0da4da82 100644 --- a/src/packages/cascader/demos/taro/demo1.tsx +++ b/src/packages/cascader/demos/taro/demo1.tsx @@ -1,99 +1,102 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react-taro' const Demo1 = () => { - const [isVisibleDemo1, setIsVisibleDemo1] = useState(false) - const [value1, setValue1] = useState([]) - const [optionsDemo1] = useState([ - { - value: '浙江', - text: '浙江', - children: [ - { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setOptions([ { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '芙蓉区', text: '芙蓉区' }, - { value: '岳麓区', text: '岳麓区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, ], }, { - value: '岳阳', - text: '岳阳', + value: '福建', + text: '福建', children: [ - { value: '岳阳楼区', text: '岳阳楼区' }, - { value: '云溪区', text: '云溪区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ - { - value: '福州', - text: '福州', - children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, - ], - }, - ], - }, - ]) - const change1 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue1(value) - } - + ]) + }, 300) + }, []) return ( <> { - setIsVisibleDemo1(true) + setVisible(true) }} /> { - setIsVisibleDemo1(false) + setVisible(false) }} - onChange={change1} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/taro/demo2.tsx b/src/packages/cascader/demos/taro/demo2.tsx index 2ad2a411a1..7c328c3ebc 100644 --- a/src/packages/cascader/demos/taro/demo2.tsx +++ b/src/packages/cascader/demos/taro/demo2.tsx @@ -1,91 +1,95 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react-taro' const Demo2 = () => { - const [isVisibleDemo2, setIsVisibleDemo2] = useState(false) - const [value2, setValue2] = useState(['福建', '福州', '台江区']) - const [optionsDemo2] = useState([ - { - value1: '浙江', - text1: '浙江', - items: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['福建', '福州', '台江区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + setOptions([ { - value1: '杭州', - text1: '杭州', - disabled: true, - items: [ - { value1: '西湖区', text1: '西湖区', disabled: true }, - { value1: '余杭区', text1: '余杭区' }, - ], - }, - { - value1: '温州', - text1: '温州', + value1: '浙江', + text1: '浙江', items: [ - { value1: '鹿城区', text1: '鹿城区' }, - { value1: '瓯海区', text1: '瓯海区' }, + { + value1: '杭州', + text1: '杭州', + disabled: true, + items: [ + { value1: '西湖区', text1: '西湖区', disabled: true }, + { value1: '余杭区', text1: '余杭区' }, + ], + }, + { + value1: '温州', + text1: '温州', + items: [ + { value1: '鹿城区', text1: '鹿城区' }, + { value1: '瓯海区', text1: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value1: '湖南', - text1: '湖南', - disabled: true, - items: [ { - value1: '长沙', - text1: '长沙', + value1: '湖南', + text1: '湖南', disabled: true, items: [ - { value1: '芙蓉区', text1: '芙蓉区' }, - { value1: '岳麓区', text1: '岳麓区' }, - ], - }, - { - value1: '岳阳', - text1: '岳阳', - children: [ - { value1: '岳阳楼区', text1: '岳阳楼区' }, - { value1: '云溪区', text1: '云溪区' }, + { + value1: '长沙', + text1: '长沙', + disabled: true, + items: [ + { value1: '芙蓉区', text1: '芙蓉区' }, + { value1: '岳麓区', text1: '岳麓区' }, + ], + }, + { + value1: '岳阳', + text1: '岳阳', + children: [ + { value1: '岳阳楼区', text1: '岳阳楼区' }, + { value1: '云溪区', text1: '云溪区' }, + ], + }, ], }, - ], - }, - { - value1: '福建', - text1: '福建', - items: [ { - value1: '福州', - text1: '福州', + value1: '福建', + text1: '福建', items: [ - { value1: '鼓楼区', text1: '鼓楼区' }, - { value1: '台江区', text1: '台江区' }, + { + value1: '福州', + text1: '福州', + items: [ + { value1: '鼓楼区', text1: '鼓楼区' }, + { value1: '台江区', text1: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change2 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue2(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo2(true) + setVisible(true) }} /> { }} closeable onClose={() => { - setIsVisibleDemo2(false) + setVisible(false) }} - onChange={change2} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/taro/demo3.tsx b/src/packages/cascader/demos/taro/demo3.tsx index b087ba3247..c89357afb5 100644 --- a/src/packages/cascader/demos/taro/demo3.tsx +++ b/src/packages/cascader/demos/taro/demo3.tsx @@ -1,56 +1,51 @@ import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react-taro' const Demo3 = () => { - const [isVisibleDemo3, setIsVisibleDemo3] = useState(false) - const [value3, setValue3] = useState(['A0', 'A12', 'A23', 'A32']) + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['A11', 'A21', 'A31', 'A41']) - const lazyLoadDemo3 = (node: any, resolve: (children: any) => void) => { - setTimeout(() => { - if (node.root) { - resolve([ - { value: 'A0', text: 'A0' }, - { value: 'B0', text: 'B0' }, - { value: 'C0', text: 'C0' }, - ]) - } else { - const { value, level } = node - const text = value.substring(0, 1) + const loadCascaderItemData = ( + node: CascaderOption, + level: number + ): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value?.toString().substring(0, 1) const value1 = `${text}${level + 1}1` const value2 = `${text}${level + 1}2` - const value3 = `${text}${level + 1}3` resolve([ - { value: value1, text: value1, leaf: level >= 6 }, - { value: value2, text: value2, leaf: level >= 6 }, - { value: value3, text: value3, leaf: level >= 6 }, + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 2 }, ]) - } - }, 2000) + }, 500) + }) } - const change3 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue3(value) + const onChange = (value: any, path: any) => { + setValue(value) } + return ( <> { - setIsVisibleDemo3(true) + setVisible(true) }} /> { - setIsVisibleDemo3(false) + setVisible(false) }} - onChange={change3} + onChange={onChange} lazy - onLoad={lazyLoadDemo3} + onLoad={loadCascaderItemData} /> ) diff --git a/src/packages/cascader/demos/taro/demo4.tsx b/src/packages/cascader/demos/taro/demo4.tsx index a22c6c7aeb..225a3e9807 100644 --- a/src/packages/cascader/demos/taro/demo4.tsx +++ b/src/packages/cascader/demos/taro/demo4.tsx @@ -1,59 +1,67 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react-taro' const Demo4 = () => { - const [isVisibleDemo4, setIsVisibleDemo4] = useState(false) - const [value4, setValue4] = useState([]) - const [optionsDemo4] = useState([ - { value: 'A0', text: 'A0' }, - { - value: 'B0', - text: 'B0', - children: [ - { value: 'B11', text: 'B11', leaf: true }, - { value: 'B12', text: 'B12' }, - ], - }, - { value: 'C0', text: 'C0' }, - ]) - - const lazyLoadDemo4 = (node: any, resolve: (children: any) => void) => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + useEffect(() => { setTimeout(() => { - const { value, level } = node - const text = value.substring(0, 1) - const value1 = `${text}${level + 1}1` - const value2 = `${text}${level + 1}2` - resolve([ - { value: value1, text: value1, leaf: level >= 2 }, - { value: value2, text: value2, leaf: level >= 1 }, + setOptions([ + { value: 'A0', text: 'A0' }, + { + value: 'B0', + text: 'B0', + children: [ + { value: 'B11', text: 'B11', leaf: true }, + { value: 'B12', text: 'B12' }, + ], + }, + { value: 'C0', text: 'C0' }, ]) - }, 500) + }, 300) + }, []) + + const lazyLoadDemo4 = async ( + node: any, + level: number + ): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const { value } = node + const text = value.substring(0, 1) + const value1 = `${text}${level + 1}1` + const value2 = `${text}${level + 1}2` + resolve([ + { value: value1, text: value1, leaf: level >= 2 }, + { value: value2, text: value2, leaf: level >= 1 }, + ]) + }, 500) + }) } const change4 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue4(value) + setValue(value) } return ( <> { - setIsVisibleDemo4(true) + setVisible(true) }} /> { - setIsVisibleDemo4(false) + setVisible(false) }} onChange={change4} - lazy onLoad={lazyLoadDemo4} /> diff --git a/src/packages/cascader/demos/taro/demo5.tsx b/src/packages/cascader/demos/taro/demo5.tsx index 4df6f2023b..79ef7e20d5 100644 --- a/src/packages/cascader/demos/taro/demo5.tsx +++ b/src/packages/cascader/demos/taro/demo5.tsx @@ -1,47 +1,55 @@ -import React, { useState } from 'react' -import { Cascader, Cell } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { Cascader, CascaderOption, Cell } from '@nutui/nutui-react-taro' const Demo5 = () => { - const [isVisibleDemo5, setIsVisibleDemo5] = useState(false) - const [value5, setValue5] = useState(['广东省', '广州市']) - const [optionsDemo5] = useState([ - { value: '北京', text: '北京', id: 1, pidd: null }, - { value: '通州区', text: '通州区', id: 11, pidd: 1 }, - { value: '经海路', text: '经海路', id: 111, pidd: 11 }, - { value: '广东省', text: '广东省', id: 2, pidd: null }, - { value: '广州市', text: '广州市', id: 21, pidd: 2 }, - ]) - const [convertConfigDemo5, setConvertConfigDemo5] = useState({ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const format = { topId: null, idKey: 'id', pidKey: 'pidd', - sortKey: '', - }) - const change5 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue5(value) + } + + useEffect(() => { + setTimeout(() => { + setValue(['广东省', '广州市']) + setOptions([ + { value: '北京', text: '北京', id: 1, pidd: null }, + { value: '通州区', text: '通州区', id: 11, pidd: 1, sortKey: 2 }, + { value: '大兴区', text: '大兴区', id: 12, pidd: 1, sortKey: 1 }, + { value: '经海路', text: '经海路', id: 111, pidd: 12, sortKey: 2 }, + { value: '黄亦路', text: '黄亦路', id: 112, pidd: 12, sortKey: 1 }, + { value: '广东省', text: '广东省', id: 2, pidd: null }, + { value: '广州市', text: '广州市', id: 21, pidd: 2 }, + ]) + }, 300) + }, []) + + const onChange = (value: any, path: any) => { + setValue(value) } return ( <> { - setIsVisibleDemo5(true) + setVisible(true) }} /> { - setIsVisibleDemo5(false) + setVisible(false) }} - onChange={change5} + onChange={onChange} /> ) diff --git a/src/packages/cascader/demos/taro/demo6.tsx b/src/packages/cascader/demos/taro/demo6.tsx index 20c66f81bb..bf0d742cba 100644 --- a/src/packages/cascader/demos/taro/demo6.tsx +++ b/src/packages/cascader/demos/taro/demo6.tsx @@ -1,5 +1,10 @@ -import React, { useState } from 'react' -import { Cell, Cascader, ConfigProvider } from '@nutui/nutui-react-taro' +import React, { useEffect, useState } from 'react' +import { + Cell, + Cascader, + ConfigProvider, + CascaderOption, +} from '@nutui/nutui-react-taro' const customTheme = { nutuiCascaderItemHeight: '48px', @@ -11,74 +16,79 @@ const customTheme = { } const Demo6 = () => { - const [isVisibleDemo6, setIsVisibleDemo6] = useState(false) - const [value6, setValue6] = useState([]) - const [optionsDemo6] = useState([ - { - value: '浙江', - text: '浙江', - children: [ + const [visible, setVisible] = useState(false) + const [value, setValue] = useState(['浙江', '温州', '鹿城区']) + const [options, setOptions] = useState([]) + useEffect(() => { + setTimeout(() => { + // setValue(['浙江', '温州', '鹿城区']) + setOptions([ { - value: '杭州', - text: '杭州', - disabled: true, - children: [ - { value: '西湖区', text: '西湖区', disabled: true }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', + value: '浙江', + text: '浙江', children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '湖南', - text: '湖南', - disabled: true, - children: [ { - value: '长沙', - text: '长沙', + value: '湖南', + text: '湖南', disabled: true, children: [ - { value: '西湖区', text: '西湖区' }, - { value: '余杭区', text: '余杭区' }, - ], - }, - { - value: '温州', - text: '温州', - children: [ - { value: '鹿城区', text: '鹿城区' }, - { value: '瓯海区', text: '瓯海区' }, + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区' }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, ], }, - ], - }, - { - value: '福建', - text: '福建', - children: [ { - value: '福州', - text: '福州', + value: '福建', + text: '福建', children: [ - { value: '鼓楼区', text: '鼓楼区' }, - { value: '台江区', text: '台江区' }, + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, ], }, - ], - }, - ]) - const change6 = (value: any, path: any) => { - console.log('onChange', value, path) - setValue6(value) + ]) + }, 300) + }, []) + const onChange = (value: any, path: any) => { + setValue(value) } const onPathChange = (value: any, path: any) => { console.log('onPathChange', value, path) @@ -88,26 +98,26 @@ const Demo6 = () => { <> { - setIsVisibleDemo6(true) + setVisible(true) }} /> { - setIsVisibleDemo6(false) + setVisible(false) }} - onChange={change6} + onChange={onChange} onPathChange={onPathChange} - />{' '} + /> ) diff --git a/src/packages/cascader/types.ts b/src/packages/cascader/types.ts index de37a59bd0..3e57cfb998 100644 --- a/src/packages/cascader/types.ts +++ b/src/packages/cascader/types.ts @@ -1,6 +1,3 @@ -import { ReactNode } from 'react' -import { PopupProps } from '@/packages/popup' - export interface CascaderPane { nodes: [] selectedNode: CascaderOption | null @@ -44,50 +41,3 @@ export type CascaderActions = { open: () => void close: () => void } -export type CascaderPopupProps = Pick< - PopupProps, - | 'className' - | 'style' - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' -> -export type CascaderSupportPopupProps = Partial< - Omit< - PopupProps, - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > -> - -export interface CascaderProps extends CascaderPopupProps { - visible: boolean - value: CascaderValue - activeColor: string - activeIcon: ReactNode - defaultValue: CascaderValue - options: CascaderOption[] - optionKey: CascaderOptionKey - format: Record - closeable: boolean - closeIcon: ReactNode - closeIconPosition: string - popup: boolean - popupProps: CascaderSupportPopupProps - lazy: boolean - onLoad: ( - node: CascaderOption, - levelIndex: number - ) => Promise - onChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void - onPathChange: (value: CascaderValue, pathNodes: CascaderOption[]) => void - onTabsChange: (index: number) => void - onClose: () => void -} From 22bdf63b90d36cd0dbbeb18b6edd9a48066ce334 Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 16:07:44 +0800 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/doc.en-US.md | 12 ++++++++---- src/packages/cascader/doc.md | 12 ++++++++---- src/packages/cascader/doc.taro.md | 12 ++++++++---- src/packages/cascader/doc.zh-TW.md | 12 ++++++++---- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/packages/cascader/doc.en-US.md b/src/packages/cascader/doc.en-US.md index e382f5d190..f5e1a8cf5d 100644 --- a/src/packages/cascader/doc.en-US.md +++ b/src/packages/cascader/doc.en-US.md @@ -32,7 +32,8 @@ use `optionKey` Specify the property name. ### Async loading -Use `lazy` to identify whether data needs to be obtained dynamically. At this time, not transmitting `options` means that all data needs to be loaded through `lazyload` . The first loading is distinguished by the `root` attribute. When a non leaf node is encountered, the `lazyload` method will be called. The parameters are the current node and the `resolve` method. Note that the `resolve` method must be called. If it is not transmitted to a child node, it will be treated as a leaf node. +The `lazy` attribute indicates that automatic data loading is enabled. Cascader implements the logic of automatic data loading through `value` and `onLoad`. The `lazy` attribute must be set at the same time as the `onLoad` attribute. +The data type returned by the `onLoad` method is `CascaderOption[]` :::demo @@ -42,6 +43,9 @@ Use `lazy` to identify whether data needs to be obtained dynamically. At this ti ### Async loading of partial data +Dynamic loading of some data means that the initial `options` has been set. For example, the data of the user's current address is first obtained and assigned to `options`. The data loading when the user switches provinces or regions is implemented through the `onLoad` method. +**No need to set the `lazy` attribute. ** + :::demo @@ -88,9 +92,9 @@ Use configprovider to set custom CSS | closeIconPosition | Cancel the button position and inherit the popup component | `string` | `top-right` | | closeIcon | Customize the close button and inherit the popup component | `ReactNode` | `close` | | closeable | Whether to display the close button and inherit the popup component | `boolean` | `true` | -| onLoad | Dynamic loading callback, which takes effect when dynamic loading is enabled | `(node: any, resolve: any) => void` | `-` | -| onChange | Triggered when the selected value changes | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | Triggered when the selected item changes | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | Dynamic loading callback, which takes effect when dynamic loading is enabled | `(node: any) => void` | `-` | +| onChange | Triggered when the selected value changes | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | Triggered when the selected item changes | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/doc.md b/src/packages/cascader/doc.md index 8758db7ea1..2144c31c96 100644 --- a/src/packages/cascader/doc.md +++ b/src/packages/cascader/doc.md @@ -32,7 +32,8 @@ import { Cascader } from '@nutui/nutui-react' ### 动态加载 -使用`lazy`标识是否需要动态获取数据,此时不传`options`代表所有数据都需要通过`onLoad`加载,首次加载通过`root`属性区分,当遇到非叶子节点时会调用`onLoad`方法,参数为当前节点和`resolve`方法,注意`resolve`方法必须调用,不传子节点时会被当做叶子节点处理。 +`lazy` 属性表示开启数据的自动加载,Cascader 内部通过 `value` 和 `onLoad` 实现了自动加载数据的逻辑。`lazy` 属性必须和 `onLoad` 属性同时设置。 +`onLoad`方法返回的数据类型为 `CascaderOption[]` :::demo @@ -42,6 +43,9 @@ import { Cascader } from '@nutui/nutui-react' ### 部分数据动态加载 +部分数据动态加载是指已经设置了初始的 `options`,例如,首先获取了用户当前地址的数据,并赋值给 `options`,用户切换省份或地区的数据加载则通过 `onLoad` 方法实现。 +**无需设置 `lazy` 属性。** + :::demo @@ -88,9 +92,9 @@ import { Cascader } from '@nutui/nutui-react' | closeIconPosition | 取消按钮位置,继承 Popup 组件 | `string` | `top-right` | | closeIcon | 自定义关闭按钮,继承 Popup 组件 | `ReactNode` | `close` | | closeable | 是否显示关闭按钮,继承 Popup 组件 | `boolean` | `true` | -| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any, resolve: any) => void` | `-` | -| onChange | 选中值改变时触发 | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | 选中项改变时触发 | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any) => void` | `-` | +| onChange | 选中值改变时触发 | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | 选中项改变时触发 | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/doc.taro.md b/src/packages/cascader/doc.taro.md index 3d729965ff..c931917537 100644 --- a/src/packages/cascader/doc.taro.md +++ b/src/packages/cascader/doc.taro.md @@ -32,7 +32,8 @@ import { Cascader } from '@nutui/nutui-react-taro' ### 动态加载 -使用`lazy`标识是否需要动态获取数据,此时不传`options`代表所有数据都需要通过`onLoad`加载,首次加载通过`root`属性区分,当遇到非叶子节点时会调用`onLoad`方法,参数为当前节点和`resolve`方法,注意`resolve`方法必须调用,不传子节点时会被当做叶子节点处理。 +`lazy` 属性表示开启数据的自动加载,Cascader 内部通过 `value` 和 `onLoad` 实现了自动加载数据的逻辑。`lazy` 属性必须和 `onLoad` 属性同时设置。 +`onLoad`方法返回的数据类型为 `CascaderOption[]` :::demo @@ -42,6 +43,9 @@ import { Cascader } from '@nutui/nutui-react-taro' ### 部分数据动态加载 +部分数据动态加载是指已经设置了初始的 `options`,例如,首先获取了用户当前地址的数据,并赋值给 `options`,用户切换省份或地区的数据加载则通过 `onLoad` 方法实现。 +**无需设置 `lazy` 属性。** + :::demo @@ -88,9 +92,9 @@ import { Cascader } from '@nutui/nutui-react-taro' | closeIconPosition | 取消按钮位置,继承 Popup 组件 | `string` | `top-right` | | closeIcon | 自定义关闭按钮,继承 Popup 组件 | `ReactNode` | `close` | | closeable | 是否显示关闭按钮,继承 Popup 组件 | `boolean` | `true` | -| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any, resolve: any) => void` | `-` | -| onChange | 选中值改变时触发 | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | 选中项改变时触发 | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | 动态加载回调,开启动态加载时生效 | `(node: any) => void` | `-` | +| onChange | 选中值改变时触发 | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | 选中项改变时触发 | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref diff --git a/src/packages/cascader/doc.zh-TW.md b/src/packages/cascader/doc.zh-TW.md index 7ac7f7104f..f5bb755c87 100644 --- a/src/packages/cascader/doc.zh-TW.md +++ b/src/packages/cascader/doc.zh-TW.md @@ -32,7 +32,8 @@ import { Cascader } from '@nutui/nutui-react' ### 動態加載 -使用`lazy`標識是否需要動態獲取數據,此時不傳`options`代表所有數據都需要通過`onLoad`加載,首次加載通過`root`屬性區分,當遇到非葉子節點時會調用`onLoad`方法,參數為當前節點和`resolve`方法,註意`resolve`方法必須調用,不傳子節點時會被當做葉子節點處理。 +`lazy` 屬性表示開啟資料的自動加載,Cascader 內部透過 `value` 和 `onLoad` 實作了自動載入資料的邏輯。 `lazy` 屬性必須同時和 `onLoad` 屬性設定。 +`onLoad`方法傳回的資料類型為 `CascaderOption[]` :::demo @@ -42,6 +43,9 @@ import { Cascader } from '@nutui/nutui-react' ### 部分數據動態加載 +部分數據動態載入是指已經設定了初始的 `options`,例如,首先獲取了用戶當前地址的數據,並賦值給 `options`,用戶切換省份或地區的數據加載則透過 `onLoad` 方法實現。 +**無需設定 `lazy` 屬性。 ** + :::demo @@ -88,9 +92,9 @@ import { Cascader } from '@nutui/nutui-react' | closeIconPosition | 取消按鈕位置,繼承 Popup 組件 | `string` | `top-right` | | closeIcon | 自定義關閉按鈕,繼承 Popup 組件 | `ReactNode` | `close` | | closeable | 是否顯示關閉按鈕,繼承 Popup 組件 | `boolean` | `true` | -| onLoad | 動態加載回調,開啟動態加載時生效 | `(node: any, resolve: any) => void` | `-` | -| onChange | 選中值改變時觸發 | `(value: CascaderValue, params?: any) => void` | `-` | -| onPathChange | 選中項改變時觸發 | `(value: CascaderValue, params: any) => void` | `-` | +| onLoad | 動態加載回調,開啟動態加載時生效 | `(node: any) => void` | `-` | +| onChange | 選中值改變時觸發 | `(value: CascaderValue, pathNodes?: any) => void` | `-` | +| onPathChange | 選中項改變時觸發 | `(value: CascaderValue, pathNodes: any) => void` | `-` | ### Ref From a187e453fd87f2c4fff5d98d775a888615f3a6db Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 16:19:10 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E9=9D=9E?= =?UTF-8?q?=E5=8F=97=E6=8E=A7=E7=9A=84=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/cascader-origin.tsx | 507 --------------------- src/packages/cascader/cascader.taro.tsx | 31 +- src/packages/cascader/cascader.tsx | 29 +- src/packages/cascader/demo.taro.tsx | 6 + src/packages/cascader/demo.tsx | 6 + src/packages/cascader/demos/h5/demo1.tsx | 4 + src/packages/cascader/demos/h5/demo7.tsx | 109 +++++ src/packages/cascader/demos/taro/demo7.tsx | 109 +++++ src/packages/cascader/doc.en-US.md | 10 + src/packages/cascader/doc.md | 10 + src/packages/cascader/doc.taro.md | 10 + src/packages/cascader/doc.zh-TW.md | 10 + 12 files changed, 315 insertions(+), 526 deletions(-) delete mode 100644 src/packages/cascader/cascader-origin.tsx create mode 100644 src/packages/cascader/demos/h5/demo7.tsx create mode 100644 src/packages/cascader/demos/taro/demo7.tsx diff --git a/src/packages/cascader/cascader-origin.tsx b/src/packages/cascader/cascader-origin.tsx deleted file mode 100644 index f6c4c2f647..0000000000 --- a/src/packages/cascader/cascader-origin.tsx +++ /dev/null @@ -1,507 +0,0 @@ -import React, { - ForwardRefRenderFunction, - PropsWithChildren, - isValidElement, - useState, - useEffect, - ReactNode, - useImperativeHandle, -} from 'react' -import classNames from 'classnames' -import { Loading, Checklist } from '@nutui/icons-react' -import { Popup, PopupProps } from '@/packages/popup/popup' -import { Tabs } from '@/packages/tabs/tabs' -import { convertListToOptions } from './helper' -import { - CascaderPane, - CascaderOption, - CascaderValue, - CascaderOptionKey, - CascaderFormat, -} from './types' -import Tree from './tree' -import { ComponentDefaults } from '@/utils/typings' -import { usePropsValue } from '@/utils/use-props-value' -import { useConfig } from '@/packages/configprovider' - -export interface CascaderProps - extends Pick< - PopupProps, - | 'className' - | 'style' - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > { - popup: boolean - popupProps: Partial< - Omit< - PopupProps, - | 'closeIcon' - | 'closeable' - | 'title' - | 'left' - | 'closeIconPosition' - | 'onClose' - > - > - visible: boolean // popup visible - activeColor: string - activeIcon: string - options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue - optionKey: CascaderOptionKey - format: Record - closeable: boolean - closeIconPosition: string - closeIcon: ReactNode - lazy: boolean - onLoad: (node: any, resolve: any) => void - onChange: (value: CascaderValue, params?: any) => void - onPathChange: (value: CascaderValue, params: any) => void -} - -export type CascaderActions = { - open: () => void - close: () => void -} - -const defaultProps = { - ...ComponentDefaults, - activeColor: '', - activeIcon: 'checklist', - popup: true, - options: [], - optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, - format: {}, - closeable: false, - closeIconPosition: 'top-right', - closeIcon: 'close', - lazy: false, - onLoad: () => {}, - onClose: () => {}, - onChange: () => {}, - onPathChange: () => {}, -} as unknown as CascaderProps -const InternalCascader: ForwardRefRenderFunction< - unknown, - PropsWithChildren> -> = (props, ref) => { - const { locale } = useConfig() - const { - className, - style, - activeColor, - activeIcon, - popup, - popupProps = {}, - visible, - options, - value, - defaultValue, - optionKey, - format, - closeable, - closeIconPosition, - closeIcon, - lazy, - title, - left, - onLoad, - onClose, - onChange, - onPathChange, - } = { ...defaultProps, ...props } - - const [tabvalue, setTabvalue] = useState('c1') - const [optionsData, setOptionsData] = useState([]) - const isLazy = () => state.configs.lazy && Boolean(state.configs.onLoad) - - const [innerValue, setInnerValue] = usePropsValue({ - value, - defaultValue, - finalValue: defaultValue, - }) - const [innerVisible, setInnerVisible] = usePropsValue({ - value: visible, - defaultValue: undefined, - finalValue: false, - }) - const actions: CascaderActions = { - open: () => { - setInnerVisible(true) - }, - close: () => { - setInnerVisible(false) - }, - } - useImperativeHandle(ref, () => actions) - - const [state] = useState({ - optionsData: [] as any, - panes: [ - { - nodes: [] as any, - selectedNode: [] as CascaderOption | null, - paneKey: '', - }, - ], - tree: new Tree([], {}), - tabsCursor: 0, // 选中的tab项 - initLoading: false, - currentProcessNode: [] as CascaderOption | null, - configs: { - lazy, - onLoad, - optionKey, - format, - }, - lazyLoadMap: new Map(), - }) - - const classPrefix = classNames(`nut-cascader`) - const classesPane = classNames({ - [`${classPrefix}-pane`]: true, - }) - - useEffect(() => { - initData() - }, [options, format]) - - useEffect(() => { - syncValue() - }, [value]) - - const initData = async () => { - // 初始化开始处理数据 - state.lazyLoadMap.clear() - if (format && Object.keys(format).length > 0) { - state.optionsData = convertListToOptions( - options as CascaderOption[], - format as CascaderFormat - ) - } else { - state.optionsData = options - } - state.tree = new Tree(state.optionsData as CascaderOption[], { - value: state.configs.optionKey.valueKey, - text: state.configs.optionKey.textKey, - children: state.configs.optionKey.childrenKey, - }) - if (isLazy() && !state.tree.nodes.length) { - await invokeLazyLoad({ - root: true, - loading: true, - text: '', - value: '', - }) - } - state.panes = [ - { - nodes: state.tree.nodes, - selectedNode: null, - paneKey: 'c1', - }, - ] - syncValue() - setOptionsData(state.panes) - } - // 处理有默认值时的数据 - const syncValue = async () => { - const currentValue = innerValue - - if ( - currentValue === undefined || - ![defaultValue, value].includes(currentValue) || - !state.tree.nodes.length - ) { - return - } - - if (currentValue.length === 0) { - state.tabsCursor = 0 - return - } - - let needToSync = currentValue - - if (isLazy() && Array.isArray(currentValue) && currentValue.length) { - needToSync = [] - const parent: any = state.tree.nodes.find( - (node) => node.value === currentValue[0] - ) - - if (parent) { - needToSync = [parent.value] - state.initLoading = true - - const last = await currentValue - .slice(1) - .reduce(async (p: Promise, value) => { - const parent = await p - await invokeLazyLoad(parent) - const node: any = parent?.children?.find( - (item: any) => item.value === value - ) - if (node) { - needToSync.push(value) - } - return Promise.resolve(node) - }, Promise.resolve(parent)) - await invokeLazyLoad(last) - state.initLoading = false - } - } - - if (needToSync.length && [defaultValue, value].includes(currentValue)) { - const pathNodes = state.tree.getPathNodesByValue(needToSync) - pathNodes.forEach((node, index) => { - state.tabsCursor = index - // 当有默认值时,不触发 chooseItem 里的 emit 事件 - chooseItem(node, true) - }) - } - } - - const invokeLazyLoad = async (node?: CascaderOption | void) => { - if (!node) { - return - } - - if (!state.configs.onLoad) { - node.leaf = true - return - } - - if ( - state.tree.isLeaf(node, isLazy()) || - state.tree.hasChildren(node, isLazy()) - ) { - return - } - - node.loading = true - - const parent = node.root ? null : node - let lazyLoadPromise = state.lazyLoadMap.get(node) - - if (!lazyLoadPromise) { - lazyLoadPromise = new Promise((resolve) => { - // 外部必须resolve - state.configs.onLoad?.(node, resolve) - }) - state.lazyLoadMap.set(node, lazyLoadPromise) - } - - const nodes: CascaderOption[] | void = await lazyLoadPromise - - if (Array.isArray(nodes) && nodes.length > 0) { - state.tree.updateChildren(nodes, parent) - } else { - // 如果加载完成后没有提供子节点,作为叶子节点处理 - node.leaf = true - } - node.loading = false - state.lazyLoadMap.delete(node) - } - - const close = () => { - setInnerVisible(false) - onClose && onClose() - } - - const closePopup = () => { - close() - } - - /* type: 是否是静默模式,是的话不触发事件 - tabsCursor: tab的索引 */ - const chooseItem = async (node: CascaderOption, type: boolean) => { - if ((!type && node.disabled) || !state.panes[state.tabsCursor]) { - return - } - // 如果没有子节点 - if (state.tree.isLeaf(node, isLazy())) { - node.leaf = true - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, (node.level as number) + 1) - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item.value) - onChange(optionParams, pathNodes) - onPathChange?.(optionParams, pathNodes) - setInnerValue(optionParams) - } - setOptionsData(state.panes) - close() - return - } - // 如果有子节点,滑到下一个 - if (state.tree.hasChildren(node, isLazy())) { - const level = (node.level as number) + 1 - - state.panes[state.tabsCursor].selectedNode = node - state.panes = state.panes.slice(0, level) - state.tabsCursor = level - state.panes.push({ - nodes: node.children || [], - selectedNode: null, - paneKey: `c${state.tabsCursor + 1}`, - }) - setOptionsData(state.panes) - setTabvalue(`c${state.tabsCursor + 1}`) - - if (!type) { - const pathNodes = state.panes.map((item) => item.selectedNode) - const optionParams = pathNodes.map((item: any) => item?.value) - onPathChange?.(optionParams, pathNodes) - } - return - } - state.currentProcessNode = node - if (node.loading) { - return - } - - await invokeLazyLoad(node) - if (state.currentProcessNode === node) { - state.panes[state.tabsCursor].selectedNode = node - chooseItem(node, type) - } - setOptionsData(state.panes) - } - - const renderItem = (pane: any, node: any, index: number) => { - const classPrefix2 = 'nut-cascader-item' - const checked = pane.selectedNode?.value === node.value - - const classes = classNames( - { - active: checked, - disabled: node.disabled, - }, - classPrefix2 - ) - - const classesTitle = classNames({ - [`${classPrefix2}-title`]: true, - }) - - const renderIcon = () => { - if (checked) { - if (isValidElement(activeIcon)) { - return activeIcon - } - return ( - - ) - } - return null - } - - return ( -
{ - chooseItem(node, false) - }} - > -
{node.text}
- {node.loading ? ( - - ) : ( - renderIcon() - )} -
- ) - } - - const renderTabs = () => { - return ( -
- { - return optionsData.map((pane, index) => ( -
{ - setTabvalue(pane.paneKey) - state.tabsCursor = index - }} - className={`nut-tabs-titles-item ${ - tabvalue === pane.paneKey ? 'nut-tabs-titles-item-active' : '' - }`} - key={pane.paneKey} - > - - {!state.initLoading && - state.panes.length && - pane?.selectedNode?.text} - {!state.initLoading && - state.panes.length && - !pane?.selectedNode?.text && - `${locale.select}`} - {!(!state.initLoading && state.panes.length) && 'Loading...'} - - -
- )) - }} - > - {!state.initLoading && state.panes.length ? ( - optionsData.map((pane) => ( - -
- {pane.nodes?.map((node: any, index: number) => - renderItem(pane, node, index) - )} -
-
- )) - ) : ( - -
- - )} - -
- ) - } - - return ( - <> - {popup ? ( - - {renderTabs()} - - ) : ( - renderTabs() - )} - - ) -} - -export const Cascader = React.forwardRef(InternalCascader) - -Cascader.displayName = 'NutCascader' diff --git a/src/packages/cascader/cascader.taro.tsx b/src/packages/cascader/cascader.taro.tsx index 320b81845e..dbb902d868 100644 --- a/src/packages/cascader/cascader.taro.tsx +++ b/src/packages/cascader/cascader.taro.tsx @@ -12,7 +12,6 @@ import { Checklist, Loading } from '@nutui/icons-react-taro' import classNames from 'classnames' import Tabs from '@/packages/tabs/index.taro' import Popup, { PopupProps } from '@/packages/popup/index.taro' -import { useConfig } from '@/packages/configprovider/index.taro' import { normalizeListOptions, normalizeOptions, @@ -28,6 +27,7 @@ import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/utils/use-props-value' import { isEmpty } from '@/utils/is-empty' import { getRefValue, useRefState } from '@/utils/use-ref-state' +import { useConfig } from '@/packages/configprovider' export type CascaderPopupProps = Pick< PopupProps, @@ -127,10 +127,12 @@ export const Cascader = forwardRef((props: Partial, ref) => { finalValue: [], onChange: (value) => { props.onChange?.(value, pathNodes.current) - props.onPathChange?.(value, pathNodes.current) + // props.onPathChange?.(value, pathNodes.current) }, }) + const [innerValue, setInnerValue] = useState(value) + const options = useMemo(() => { if (!isEmpty(format)) { return normalizeListOptions(innerOptions, format) @@ -139,7 +141,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { return normalizeOptions(innerOptions, optionKey) } return innerOptions - }, [innerOptions, optionKey, format, value]) + }, [innerOptions, optionKey, format, innerValue]) const pathNodes = useRef([]) @@ -147,7 +149,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { const next = [] let end = false let currentOptions = options - for (const [index, val] of value.entries()) { + for (const [index, val] of innerValue.entries()) { const opt = currentOptions?.find((o: CascaderOption) => o.value === val) next.push({ selected: val, @@ -168,7 +170,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { }) } return next - }, [value, options, innerOptions]) + }, [innerValue, options, innerOptions]) const [visible, setVisible] = usePropsValue({ value: outerVisible, @@ -189,13 +191,19 @@ export const Cascader = forwardRef((props: Partial, ref) => { } useImperativeHandle(ref, () => actions) + useEffect(() => { + if (!visible) { + setInnerValue(value) + } + }, [visible, value]) + useEffect(() => { setInnerOptions(outerOptions) }, [outerOptions]) useEffect(() => { setTabActiveIndex(levels.length - 1) - }, [value, innerOptions, outerOptions]) + }, [innerValue, innerOptions, outerOptions]) useEffect(() => { const max = levels.length - 1 if (tabActiveIndex > max) { @@ -206,11 +214,11 @@ export const Cascader = forwardRef((props: Partial, ref) => { const load = async () => { const parent = { children: [] } try { - await value.reduce(async (promise: Promise, val, key) => { + await innerValue.reduce(async (promise: Promise, val, key) => { const pane = await onLoad({ value: val }, key) const parent = await promise parent.children = pane - if (key === value.length - 1) { + if (key === innerValue.length - 1) { return Promise.resolve(parent) } if (pane) { @@ -231,13 +239,14 @@ export const Cascader = forwardRef((props: Partial, ref) => { const chooseItem = async (pane: CascaderOption, levelIndex: number) => { if (pane.disabled) return - const nextValue = value.slice(0, levelIndex) + const nextValue = innerValue.slice(0, levelIndex) const nextPathNodes = pathNodes.current.slice(0, levelIndex) if (pane.value) { setLoading(!!onLoad && { [levelIndex]: pane.value }) nextValue[levelIndex] = pane.value nextPathNodes[levelIndex] = pane pathNodes.current = nextPathNodes + props?.onPathChange?.(nextValue, pathNodes.current) } if (onLoad) { // 叶子节点不操作 @@ -247,12 +256,14 @@ export const Cascader = forwardRef((props: Partial, ref) => { if (asyncOptions) pane.children = asyncOptions } else { setVisible(false) + setValue(nextValue) } } if (!pane.children && !onLoad) { setVisible(false) + setValue(nextValue) } - setValue(nextValue) + setInnerValue(nextValue) setLoading({}) } diff --git a/src/packages/cascader/cascader.tsx b/src/packages/cascader/cascader.tsx index 09da209648..6defd290a4 100644 --- a/src/packages/cascader/cascader.tsx +++ b/src/packages/cascader/cascader.tsx @@ -127,10 +127,12 @@ export const Cascader = forwardRef((props: Partial, ref) => { finalValue: [], onChange: (value) => { props.onChange?.(value, pathNodes.current) - props.onPathChange?.(value, pathNodes.current) + // props.onPathChange?.(value, pathNodes.current) }, }) + const [innerValue, setInnerValue] = useState(value) + const options = useMemo(() => { if (!isEmpty(format)) { return normalizeListOptions(innerOptions, format) @@ -139,7 +141,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { return normalizeOptions(innerOptions, optionKey) } return innerOptions - }, [innerOptions, optionKey, format, value]) + }, [innerOptions, optionKey, format, innerValue]) const pathNodes = useRef([]) @@ -147,7 +149,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { const next = [] let end = false let currentOptions = options - for (const [index, val] of value.entries()) { + for (const [index, val] of innerValue.entries()) { const opt = currentOptions?.find((o: CascaderOption) => o.value === val) next.push({ selected: val, @@ -168,7 +170,7 @@ export const Cascader = forwardRef((props: Partial, ref) => { }) } return next - }, [value, options, innerOptions]) + }, [innerValue, options, innerOptions]) const [visible, setVisible] = usePropsValue({ value: outerVisible, @@ -189,13 +191,19 @@ export const Cascader = forwardRef((props: Partial, ref) => { } useImperativeHandle(ref, () => actions) + useEffect(() => { + if (!visible) { + setInnerValue(value) + } + }, [visible, value]) + useEffect(() => { setInnerOptions(outerOptions) }, [outerOptions]) useEffect(() => { setTabActiveIndex(levels.length - 1) - }, [value, innerOptions, outerOptions]) + }, [innerValue, innerOptions, outerOptions]) useEffect(() => { const max = levels.length - 1 if (tabActiveIndex > max) { @@ -206,11 +214,11 @@ export const Cascader = forwardRef((props: Partial, ref) => { const load = async () => { const parent = { children: [] } try { - await value.reduce(async (promise: Promise, val, key) => { + await innerValue.reduce(async (promise: Promise, val, key) => { const pane = await onLoad({ value: val }, key) const parent = await promise parent.children = pane - if (key === value.length - 1) { + if (key === innerValue.length - 1) { return Promise.resolve(parent) } if (pane) { @@ -231,13 +239,14 @@ export const Cascader = forwardRef((props: Partial, ref) => { const chooseItem = async (pane: CascaderOption, levelIndex: number) => { if (pane.disabled) return - const nextValue = value.slice(0, levelIndex) + const nextValue = innerValue.slice(0, levelIndex) const nextPathNodes = pathNodes.current.slice(0, levelIndex) if (pane.value) { setLoading(!!onLoad && { [levelIndex]: pane.value }) nextValue[levelIndex] = pane.value nextPathNodes[levelIndex] = pane pathNodes.current = nextPathNodes + props?.onPathChange?.(nextValue, pathNodes.current) } if (onLoad) { // 叶子节点不操作 @@ -247,12 +256,14 @@ export const Cascader = forwardRef((props: Partial, ref) => { if (asyncOptions) pane.children = asyncOptions } else { setVisible(false) + setValue(nextValue) } } if (!pane.children && !onLoad) { setVisible(false) + setValue(nextValue) } - setValue(nextValue) + setInnerValue(nextValue) setLoading({}) } diff --git a/src/packages/cascader/demo.taro.tsx b/src/packages/cascader/demo.taro.tsx index 4227dbb103..a1e773d0e4 100644 --- a/src/packages/cascader/demo.taro.tsx +++ b/src/packages/cascader/demo.taro.tsx @@ -8,11 +8,13 @@ import Demo3 from './demos/taro/demo3' import Demo4 from './demos/taro/demo4' import Demo5 from './demos/taro/demo5' import Demo6 from './demos/taro/demo6' +import Demo7 from './demos/taro/demo7' const CascaderDemo = () => { const [translated] = useTranslate({ 'zh-CN': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定义属性名称', title2: '动态加载', title3: '部分数据动态加载', @@ -21,6 +23,7 @@ const CascaderDemo = () => { }, 'zh-TW': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定義屬性名稱', title2: '動態加載', title3: '部分數據動態加載', @@ -29,6 +32,7 @@ const CascaderDemo = () => { }, 'en-US': { basic: 'Basic Usage', + uncontrolled: 'uncontrolled', title1: 'Custom Attribute Name', title2: 'Async Loading', title3: 'Async Loading Of Partial Data', @@ -43,6 +47,8 @@ const CascaderDemo = () => {

{translated.basic}

+

{translated.uncontrolled}

+

{translated.title1}

{translated.title2}

diff --git a/src/packages/cascader/demo.tsx b/src/packages/cascader/demo.tsx index 484170b715..268f027718 100644 --- a/src/packages/cascader/demo.tsx +++ b/src/packages/cascader/demo.tsx @@ -6,11 +6,13 @@ import Demo3 from './demos/h5/demo3' import Demo4 from './demos/h5/demo4' import Demo5 from './demos/h5/demo5' import Demo6 from './demos/h5/demo6' +import Demo7 from './demos/h5/demo7' const CascaderDemo = () => { const [translated] = useTranslate({ 'zh-CN': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定义属性名称', title2: '动态加载', title3: '部分数据动态加载', @@ -19,6 +21,7 @@ const CascaderDemo = () => { }, 'zh-TW': { basic: '基础用法', + uncontrolled: '基础用法-非受控', title1: '自定義屬性名稱', title2: '動態加載', title3: '部分數據動態加載', @@ -27,6 +30,7 @@ const CascaderDemo = () => { }, 'en-US': { basic: 'Basic Usage', + uncontrolled: 'uncontrolled', title1: 'Custom Attribute Name', title2: 'Async Loading', title3: 'Async Loading Of Partial Data', @@ -40,6 +44,8 @@ const CascaderDemo = () => {

{translated.basic}

+

{translated.uncontrolled}

+

{translated.title1}

{translated.title2}

diff --git a/src/packages/cascader/demos/h5/demo1.tsx b/src/packages/cascader/demos/h5/demo1.tsx index 3ed0233098..ac49452f62 100644 --- a/src/packages/cascader/demos/h5/demo1.tsx +++ b/src/packages/cascader/demos/h5/demo1.tsx @@ -6,6 +6,7 @@ const Demo1 = () => { const [value, setValue] = useState([]) const [options, setOptions] = useState([]) const onChange = (value: any, path: any) => { + console.log('onchange', value, path) setValue(value) } useEffect(() => { @@ -97,6 +98,9 @@ const Demo1 = () => { setVisible(false) }} onChange={onChange} + onPathChange={(value, path) => { + console.log(value, path) + }} /> ) diff --git a/src/packages/cascader/demos/h5/demo7.tsx b/src/packages/cascader/demos/h5/demo7.tsx new file mode 100644 index 0000000000..64ff2669f6 --- /dev/null +++ b/src/packages/cascader/demos/h5/demo7.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' + +const Demo7 = () => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + console.log('onchange', value, path) + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setValue(['浙江', '温州', '鹿城区']) + setOptions([ + { + value: '浙江', + text: '浙江', + children: [ + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, + ], + }, + { + value: '湖南', + text: '湖南', + disabled: true, + children: [ + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, + ], + }, + { + value: '福建', + text: '福建', + children: [ + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, + ], + }, + ]) + }, 300) + }, []) + return ( + <> + { + setVisible(true) + }} + /> + { + setVisible(false) + }} + onChange={onChange} + onPathChange={(value, path) => { + console.log(value, path) + }} + /> + + ) +} +export default Demo7 diff --git a/src/packages/cascader/demos/taro/demo7.tsx b/src/packages/cascader/demos/taro/demo7.tsx new file mode 100644 index 0000000000..64ff2669f6 --- /dev/null +++ b/src/packages/cascader/demos/taro/demo7.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' + +const Demo7 = () => { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState([]) + const [options, setOptions] = useState([]) + const onChange = (value: any, path: any) => { + console.log('onchange', value, path) + setValue(value) + } + useEffect(() => { + setTimeout(() => { + setValue(['浙江', '温州', '鹿城区']) + setOptions([ + { + value: '浙江', + text: '浙江', + children: [ + { + value: '杭州', + text: '杭州', + disabled: true, + children: [ + { value: '西湖区', text: '西湖区', disabled: true }, + { value: '余杭区', text: '余杭区' }, + ], + }, + { + value: '温州', + text: '温州', + children: [ + { value: '鹿城区', text: '鹿城区' }, + { value: '瓯海区', text: '瓯海区' }, + ], + }, + ], + }, + { + value: '湖南', + text: '湖南', + disabled: true, + children: [ + { + value: '长沙', + text: '长沙', + disabled: true, + children: [ + { value: '芙蓉区', text: '芙蓉区' }, + { value: '岳麓区', text: '岳麓区' }, + ], + }, + { + value: '岳阳', + text: '岳阳', + children: [ + { value: '岳阳楼区', text: '岳阳楼区' }, + { value: '云溪区', text: '云溪区' }, + ], + }, + ], + }, + { + value: '福建', + text: '福建', + children: [ + { + value: '福州', + text: '福州', + children: [ + { value: '鼓楼区', text: '鼓楼区' }, + { value: '台江区', text: '台江区' }, + ], + }, + ], + }, + ]) + }, 300) + }, []) + return ( + <> + { + setVisible(true) + }} + /> + { + setVisible(false) + }} + onChange={onChange} + onPathChange={(value, path) => { + console.log(value, path) + }} + /> + + ) +} +export default Demo7 diff --git a/src/packages/cascader/doc.en-US.md b/src/packages/cascader/doc.en-US.md index f5e1a8cf5d..cf6e423f1a 100644 --- a/src/packages/cascader/doc.en-US.md +++ b/src/packages/cascader/doc.en-US.md @@ -20,6 +20,16 @@ Pass in the `options` list ::: +### Basic Usage - Uncontroled + +Pass in the `options` list + +:::demo + + + +::: + ### Custom attribute name use `optionKey` Specify the property name. diff --git a/src/packages/cascader/doc.md b/src/packages/cascader/doc.md index 2144c31c96..e0c5c82c61 100644 --- a/src/packages/cascader/doc.md +++ b/src/packages/cascader/doc.md @@ -20,6 +20,16 @@ import { Cascader } from '@nutui/nutui-react' ::: +### 基础用法-非受控 + +传入`options`列表 + +:::demo + + + +::: + ### 自定义属性名称 可通过`optionKey` 指定属性名。 diff --git a/src/packages/cascader/doc.taro.md b/src/packages/cascader/doc.taro.md index c931917537..a1dfb4375b 100644 --- a/src/packages/cascader/doc.taro.md +++ b/src/packages/cascader/doc.taro.md @@ -20,6 +20,16 @@ import { Cascader } from '@nutui/nutui-react-taro' ::: +### 基础用法-非受控 + +传入`options`列表 + +:::demo + + + +:: + ### 自定义属性名称 可通过`textKey`、`valueKey`、`childrenKey`指定属性名。 diff --git a/src/packages/cascader/doc.zh-TW.md b/src/packages/cascader/doc.zh-TW.md index f5bb755c87..21321550c5 100644 --- a/src/packages/cascader/doc.zh-TW.md +++ b/src/packages/cascader/doc.zh-TW.md @@ -20,6 +20,16 @@ import { Cascader } from '@nutui/nutui-react' ::: +### 基础用法-非受控 + +传入`options`列表 + +:::demo + + + +::: + ### 自定義屬性名稱 可通過`textKey`、`valueKey`、`childrenKey`指定屬性名。 From 1d7b140f2b812721d0f2b1869de4755d6f1e3973 Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Thu, 23 Jan 2025 17:48:46 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=E7=B1=BB=E5=9E=8B=E6=81=A2?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../address/__test__/__snapshots__/address.spec.tsx.snap | 2 +- src/packages/address/address.taro.tsx | 4 ++-- src/packages/address/address.tsx | 4 ++-- src/packages/address/customRender.taro.tsx | 4 ++-- src/packages/address/customRender.tsx | 4 ++-- src/packages/tabs/tabs.tsx | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap b/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap index 6411be1560..5b15e9f923 100644 --- a/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap +++ b/src/packages/address/__test__/__snapshots__/address.spec.tsx.snap @@ -2,6 +2,6 @@ exports[`Address: exist defaultIcon & selectIcon 1`] = `"
请选择地址
  • 123
    探探鱼
    182****1718
    北京市次渠镇通州区
  • 123
    探探鱼
    182****1718
    钓鱼岛钓鱼岛全区
  • 456
    探探鱼
    182****1718
    北京市大兴区科创十一街18号院京东大厦
"`; -exports[`Address: show custom 1`] = `"
选择地址
请选择
浙江
湖南
福建
"`; +exports[`Address: show custom 1`] = `"
选择地址
请选择
浙江
湖南
福建
"`; exports[`Address: show exist 1`] = `"
选择地址
  • 探探鱼
    182****1718
    北京市次渠镇通州区
  • ,
    探探鱼
    182****1718
    钓鱼岛钓鱼岛全区
  • ,
    探探鱼
    182****1718
    北京市大兴区科创十一街18号院京东大厦
"`; diff --git a/src/packages/address/address.taro.tsx b/src/packages/address/address.taro.tsx index 62d3bfdaeb..139ee36c64 100644 --- a/src/packages/address/address.taro.tsx +++ b/src/packages/address/address.taro.tsx @@ -27,8 +27,8 @@ type AddressRef = { export interface AddressProps extends CascaderProps { visible: boolean defaultVisible: boolean - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue type: string options: CascaderOption[] optionKey: CascaderOptionKey diff --git a/src/packages/address/address.tsx b/src/packages/address/address.tsx index 901c982475..1fffc889ce 100644 --- a/src/packages/address/address.tsx +++ b/src/packages/address/address.tsx @@ -27,8 +27,8 @@ type AddressRef = { export interface AddressProps extends CascaderProps { visible: boolean defaultVisible: boolean - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue type: string options: CascaderOption[] optionKey: CascaderOptionKey diff --git a/src/packages/address/customRender.taro.tsx b/src/packages/address/customRender.taro.tsx index b72e290d04..fce3d818ec 100644 --- a/src/packages/address/customRender.taro.tsx +++ b/src/packages/address/customRender.taro.tsx @@ -11,8 +11,8 @@ export interface AddressProps extends CascaderProps { visible: boolean // popup visible type: string options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue optionKey: CascaderOptionKey format: Record height: string | number diff --git a/src/packages/address/customRender.tsx b/src/packages/address/customRender.tsx index b5e22b5cb3..b5576d6da6 100644 --- a/src/packages/address/customRender.tsx +++ b/src/packages/address/customRender.tsx @@ -11,8 +11,8 @@ export interface AddressProps extends CascaderProps { visible: boolean // popup visible type: string options: CascaderOption[] - value?: CascaderValue - defaultValue?: CascaderValue + value: CascaderValue + defaultValue: CascaderValue optionKey: CascaderOptionKey format: Record height: string | number diff --git a/src/packages/tabs/tabs.tsx b/src/packages/tabs/tabs.tsx index 2ab488db1c..103cbdb4a8 100644 --- a/src/packages/tabs/tabs.tsx +++ b/src/packages/tabs/tabs.tsx @@ -24,7 +24,7 @@ export interface TabsProps extends BasicComponent { activeType: 'line' | 'smile' | 'simple' | 'card' | 'button' | 'divider' duration: number | string align: 'left' | 'right' - title: () => Element[] + title: () => JSX.Element[] onChange: (index: string | number) => void onClick: (index: string | number) => void autoHeight: boolean From ef83db8a211fee63ed89c04e1ca32fbf889bbb3c Mon Sep 17 00:00:00 2001 From: oasis-cloud Date: Fri, 24 Jan 2025 09:55:27 +0800 Subject: [PATCH 11/11] =?UTF-8?q?docs:=20=E4=BF=AE=E5=A4=8D=20codeblock=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/cascader/demos/h5/demo6.tsx | 1 - src/packages/cascader/demos/taro/demo6.tsx | 1 - src/packages/cascader/demos/taro/demo7.tsx | 2 +- src/packages/cascader/doc.taro.md | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/packages/cascader/demos/h5/demo6.tsx b/src/packages/cascader/demos/h5/demo6.tsx index 49efbea558..e452791c8b 100644 --- a/src/packages/cascader/demos/h5/demo6.tsx +++ b/src/packages/cascader/demos/h5/demo6.tsx @@ -21,7 +21,6 @@ const Demo6 = () => { const [options, setOptions] = useState([]) useEffect(() => { setTimeout(() => { - // setValue(['浙江', '温州', '鹿城区']) setOptions([ { value: '浙江', diff --git a/src/packages/cascader/demos/taro/demo6.tsx b/src/packages/cascader/demos/taro/demo6.tsx index bf0d742cba..a1687fd6a4 100644 --- a/src/packages/cascader/demos/taro/demo6.tsx +++ b/src/packages/cascader/demos/taro/demo6.tsx @@ -21,7 +21,6 @@ const Demo6 = () => { const [options, setOptions] = useState([]) useEffect(() => { setTimeout(() => { - // setValue(['浙江', '温州', '鹿城区']) setOptions([ { value: '浙江', diff --git a/src/packages/cascader/demos/taro/demo7.tsx b/src/packages/cascader/demos/taro/demo7.tsx index 64ff2669f6..7160f12a7d 100644 --- a/src/packages/cascader/demos/taro/demo7.tsx +++ b/src/packages/cascader/demos/taro/demo7.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react' +import { Cascader, Cell, CascaderOption } from '@nutui/nutui-react-taro' const Demo7 = () => { const [visible, setVisible] = useState(false) diff --git a/src/packages/cascader/doc.taro.md b/src/packages/cascader/doc.taro.md index a1dfb4375b..d50fa543f3 100644 --- a/src/packages/cascader/doc.taro.md +++ b/src/packages/cascader/doc.taro.md @@ -28,7 +28,7 @@ import { Cascader } from '@nutui/nutui-react-taro' -:: +::: ### 自定义属性名称