From f8ec092982af715fb5d404a2a82bd52bf4c4ebd3 Mon Sep 17 00:00:00 2001 From: hbztd <38969912+hbztd@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:51:11 +0800 Subject: [PATCH 1/5] fix: AreaPicker exists blank --- packages/core/src/picker/picker.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/src/picker/picker.tsx b/packages/core/src/picker/picker.tsx index 559def333..47b6d9d17 100644 --- a/packages/core/src/picker/picker.tsx +++ b/packages/core/src/picker/picker.tsx @@ -10,6 +10,7 @@ import { prefixClassname } from "../styles" import { useRefs, useToRef } from "../utils/state" import { isElementOf } from "../utils/validate" import { unitToPx } from "../utils/format/unit" +import AreaPickerColumns from "../area-picker/area-picker-columns" import PickerColumns from "./picker-columns" import PickerToolbar from "./picker-toolbar" import PickerTitle from "./picker-title" @@ -115,6 +116,7 @@ function Picker(props: PickerProps) { let toolbar: ReactNode = null const __children__: ReactNode[] = [] const columns: ReactNode[] = [] + let hasAreaColumnsComp = false Children.toArray(childrenProp).forEach((child: ReactNode) => { if (isElementOf(child, PickerColumn)) { const element = child as ReactElement @@ -122,6 +124,9 @@ function Picker(props: PickerProps) { } else if (isElementOf(child, PickerToolbar)) { toolbar = child } else { + if (isElementOf(child, AreaPickerColumns)) { + hasAreaColumnsComp = true + } __children__.push(child) } }) @@ -132,12 +137,15 @@ function Picker(props: PickerProps) { {confirmText} } - if (_.isEmpty(columns) && columnsProp && columnsProp.length > 0) { + if (!hasAreaColumnsComp && _.isEmpty(columns) && columnsProp && columnsProp.length > 0) { (Array.isArray(columnsProp[0]) ? columnsProp : [columnsProp]).forEach((col, i) => { columns.push({col.map((data, ii) => )}) }) } - __children__.unshift() + if (!hasAreaColumnsComp) { + __children__.unshift() + } + __children__.unshift(toolbar) return __children__ }, [childrenProp, title, confirmText, cancelText, columnsProp, fieldNames]) From a6a043e09d4706409d4b29fa4c259d0e8c37674b Mon Sep 17 00:00:00 2001 From: hbztd <38969912+hbztd@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:09:32 +0800 Subject: [PATCH 2/5] fix: PickerOption text ellipsis does not work --- packages/core/src/picker/picker-option.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/src/picker/picker-option.tsx b/packages/core/src/picker/picker-option.tsx index ecee6c599..18f815836 100644 --- a/packages/core/src/picker/picker-option.tsx +++ b/packages/core/src/picker/picker-option.tsx @@ -30,18 +30,23 @@ export default function PickerOption(props: PickerOptionProps) { const { optionHeight } = useContext(PickerContext) const children = useMemo(() => { - if (isValidElement(childrenProp) || isTextElement(childrenProp)) { + if (isValidElement(childrenProp)) { return childrenProp } - if (isValidElement(label) || isTextElement(label)) { + if (isTextElement(childrenProp)) { + return {childrenProp} + } + if (isValidElement(label)) { return label } + if (isTextElement(label)) { + return {label} + } }, [childrenProp, label]) return ( Date: Tue, 21 Nov 2023 18:42:33 +0800 Subject: [PATCH 3/5] Revert "fix: AreaPicker exists blank" This reverts commit f8ec092982af715fb5d404a2a82bd52bf4c4ebd3. --- packages/core/src/picker/picker.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/core/src/picker/picker.tsx b/packages/core/src/picker/picker.tsx index 47b6d9d17..559def333 100644 --- a/packages/core/src/picker/picker.tsx +++ b/packages/core/src/picker/picker.tsx @@ -10,7 +10,6 @@ import { prefixClassname } from "../styles" import { useRefs, useToRef } from "../utils/state" import { isElementOf } from "../utils/validate" import { unitToPx } from "../utils/format/unit" -import AreaPickerColumns from "../area-picker/area-picker-columns" import PickerColumns from "./picker-columns" import PickerToolbar from "./picker-toolbar" import PickerTitle from "./picker-title" @@ -116,7 +115,6 @@ function Picker(props: PickerProps) { let toolbar: ReactNode = null const __children__: ReactNode[] = [] const columns: ReactNode[] = [] - let hasAreaColumnsComp = false Children.toArray(childrenProp).forEach((child: ReactNode) => { if (isElementOf(child, PickerColumn)) { const element = child as ReactElement @@ -124,9 +122,6 @@ function Picker(props: PickerProps) { } else if (isElementOf(child, PickerToolbar)) { toolbar = child } else { - if (isElementOf(child, AreaPickerColumns)) { - hasAreaColumnsComp = true - } __children__.push(child) } }) @@ -137,15 +132,12 @@ function Picker(props: PickerProps) { {confirmText} } - if (!hasAreaColumnsComp && _.isEmpty(columns) && columnsProp && columnsProp.length > 0) { + if (_.isEmpty(columns) && columnsProp && columnsProp.length > 0) { (Array.isArray(columnsProp[0]) ? columnsProp : [columnsProp]).forEach((col, i) => { columns.push({col.map((data, ii) => )}) }) } - if (!hasAreaColumnsComp) { - __children__.unshift() - } - + __children__.unshift() __children__.unshift(toolbar) return __children__ }, [childrenProp, title, confirmText, cancelText, columnsProp, fieldNames]) From 09070e1eb820fb7840445317920a2abb0bfdd1c1 Mon Sep 17 00:00:00 2001 From: hbztd <38969912+hbztd@users.noreply.github.com> Date: Wed, 22 Nov 2023 21:18:47 +0800 Subject: [PATCH 4/5] refactor: AreaPicker --- packages/core/src/area-picker/README.md | 85 ++++------ .../src/area-picker/area-picker-columns.tsx | 64 +------- .../src/area-picker/area-picker.context.ts | 11 -- .../src/area-picker/area-picker.shared.ts | 99 ++++++++++++ packages/core/src/area-picker/area-picker.tsx | 151 ++++++++++++++++-- packages/core/src/picker/index.ts | 2 +- packages/core/src/picker/picker.tsx | 13 +- .../demo/src/pages/form/area-picker/index.tsx | 39 ++--- 8 files changed, 283 insertions(+), 181 deletions(-) delete mode 100644 packages/core/src/area-picker/area-picker.context.ts create mode 100644 packages/core/src/area-picker/area-picker.shared.ts diff --git a/packages/core/src/area-picker/README.md b/packages/core/src/area-picker/README.md index d0f0d6adb..bc19eec2a 100644 --- a/packages/core/src/area-picker/README.md +++ b/packages/core/src/area-picker/README.md @@ -14,24 +14,10 @@ import { AreaPicker } from "@taroify/core" ### 基础用法 -初始化省市区组件时,需要通过 `AreaPicker.Columns` 子组件传入省市区数据。 +初始化省市区组件时,需要通过 `areaList` 属性传入省市区数据。 ```tsx -import { AreaPicker } from "@taroify/core" -import { areaList } from "@vant/area-data" - -function BasicAreaPicker() { - return ( - - - 取消 - 标题 - 确认 - - - - ) -} + ``` ### areaList 格式 @@ -80,22 +66,12 @@ import { areaList } from "@vant/area-data" ``` ### 选中省市区 +通过 `defaultValue` 设置默认值 -如果想选中某个省市区,需要传入一个 `value` 属性,绑定对应的地区码。 +通过 `value` `onChange` 控制选中 ```tsx -function AreaPickerWithValue() { - return ( - - - 取消 - 标题 - 确认 - - - - ) -} + ``` ### 配置显示列 @@ -103,9 +79,18 @@ function AreaPickerWithValue() { 可以通过 `depth` 属性配置省市区显示的列数,默认情况下会显示省市区,当你设置为 `2`,则只会显示省市选择。 ```tsx -function AreaPickerWithColumns2() { + +``` + +### 手动控制DOM +初始化省市区组件时,需要通过 `AreaPicker.Columns` 子组件传入省市区数据。 +```tsx +import { AreaPicker } from "@taroify/core" +import { areaList } from "@vant/area-data" + +function BasicAreaPicker() { return ( - + 取消 标题 @@ -123,38 +108,22 @@ function AreaPickerWithColumns2() { | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| defaultValue | 默认选中的值 | _any[]_ | - | -| value | 选中的值 | _any[]_ | - | +| defaultValue | 默认选中的值 | _string[]_ | - | +| value | 选中的值 | _string[]_ | - | +| areaList | 省市区数据 | _object_ | - | +| depth | 显示列数,3-省市区,2-省市,1-省 | _string_ | `3` | +| title | 顶部栏标题 | _ReactNode_ | - | +| confirmText | 确认按钮文字 | _ReactNode_ | `确认` | +| cancelText | 取消按钮文字 | _ReactNode_| `取消` | | loading | 是否显示加载状态 | _boolean_ | `false` | | readonly | 是否为只读状态,只读状态下无法切换选项 | _boolean_ | `false` | | siblingCount | 可见的选项相邻个数 | _number_ | `3` | -| depth | 显示列数,3-省市区,2-省市,1-省 | _number \| string_ | `3` | +| optionHeight | 选项高度,支持 `px` `vw` `vh` `rem` `rpx` 单位,默认 `px` | _number\|string_ | `44` | ### Events | 事件 | 说明 | 回调参数 | | ------- | ------------------ | ------------------------------ | -| onConfirm | 点击完成按钮时触发 | _values: any[], options: PickerOptionObject[]_ | -| onCancel | 点击取消按钮时触发 | - | -| onChange | 选项改变时触发 | _values: any[], option: PickerOptionObject, column: PickerOptionObject_ | - -### PickerOptionObject 格式 - -onConfirm 事件返回的数据整体为一个数组,数组每一项对应一列选项中被选中的数据。 - -```tsx -[ - { - value: "110000", - label: "北京市", - }, - { - value: "110100", - label: "北京市", - }, - { - value: "110101", - label: "东城区", - }, -]; -``` +| onConfirm | 点击完成按钮时触发 | _values: string[], options: PickerOptionObject[]_ | +| onCancel | 点击取消按钮时触发 | _values: string[], options: PickerOptionObject[]_ | +| onChange | 选项改变时触发 | _values: string[], option: PickerOptionObject, column: PickerOptionObject_ | diff --git a/packages/core/src/area-picker/area-picker-columns.tsx b/packages/core/src/area-picker/area-picker-columns.tsx index fb48063cb..c4715ed9b 100644 --- a/packages/core/src/area-picker/area-picker-columns.tsx +++ b/packages/core/src/area-picker/area-picker-columns.tsx @@ -1,67 +1,13 @@ -import useArea, { AreaData } from "@taroify/hooks/use-area" -import * as _ from "lodash" -import * as React from "react" -import { useContext, useEffect, useRef, ReactNode } from "react" -import Picker, { PickerColumnsProps, PickerContext, PickerOptionObject } from "../picker" -import { useToRef } from "../utils/state" -import AreaPickerContext from "./area-picker.context" +import { ViewProps } from "@tarojs/components" +import { AreaData } from "./area-picker.shared" -interface AreaPickerColumnsProps extends Omit { +interface AreaPickerColumnsProps extends Omit { children?: AreaData + depth?: number } function AreaPickerColumns(props: AreaPickerColumnsProps) { - const { children: data } = props - const { depth, formatter } = useContext(AreaPickerContext) - - const { - values: unverifiedValues, - setValueOptions, - onChange, - ...restCtx // - } = useContext(PickerContext) - - const { - columns, - values, - valueOptions, - setValues, - getValueOptions, // - } = useArea(unverifiedValues, { - data, - depth, - formatter, - }) - - const onChangeRef = useToRef(onChange) - const valuesRef = useRef() - const columnRef = useRef() - - _.forEach(valueOptions, (value, index) => setValueOptions?.(value as PickerOptionObject, { index })) - - useEffect(() => { - if (columnRef.current && !_.isEqual(valuesRef.current, values)) { - const option = _.get(getValueOptions(), columnRef.current.index) - onChangeRef.current?.(values, option as PickerOptionObject, columnRef.current) - valuesRef.current = values - } - columnRef.current = undefined - }, [getValueOptions, onChangeRef, values]) - - return ( - - - - ) + return null } export default AreaPickerColumns diff --git a/packages/core/src/area-picker/area-picker.context.ts b/packages/core/src/area-picker/area-picker.context.ts deleted file mode 100644 index 9a0edaed0..000000000 --- a/packages/core/src/area-picker/area-picker.context.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AreaFormatter } from "@taroify/hooks/use-area" -import { createContext } from "react" - -interface AreaPickerContextValue { - depth?: number - formatter?: AreaFormatter -} - -const AreaPickerContext = createContext({}) - -export default AreaPickerContext diff --git a/packages/core/src/area-picker/area-picker.shared.ts b/packages/core/src/area-picker/area-picker.shared.ts new file mode 100644 index 000000000..42149aff8 --- /dev/null +++ b/packages/core/src/area-picker/area-picker.shared.ts @@ -0,0 +1,99 @@ +import type { PickerOptionData } from "../picker/picker.shared" + +export interface AreaData { + province_list: Record + city_list: Record + county_list: Record +} + +export const AREA_EMPTY_CODE = "000000"; + +const makeOption = ( + text = "", + value = AREA_EMPTY_CODE, + children: PickerOptionData[] | undefined = undefined, +): PickerOptionData => ({ + label: text, + value, + children, +}); + +export function formatDataForCascade({ + areaList, + columnsNum, + columnsPlaceholder: placeholder = [], +}: { + areaList: AreaData; + columnsNum: number; + columnsPlaceholder?: string[]; +}) { + const { + city_list: city = {}, + county_list: county = {}, + province_list: province = {}, + } = areaList; + const showCity = +columnsNum > 1; + const showCounty = +columnsNum > 2; + + const getProvinceChildren = () => { + if (showCity) { + return placeholder.length + ? [ + makeOption( + placeholder[0], + AREA_EMPTY_CODE, + showCounty ? [] : undefined, + ), + ] + : []; + } + }; + + const provinceMap = new Map(); + Object.keys(province).forEach((code) => { + provinceMap.set( + code.slice(0, 2), + makeOption(province[code], code, getProvinceChildren()), + ); + }); + + const cityMap = new Map(); + if (showCity) { + const getCityChildren = () => { + if (showCounty) { + return placeholder.length ? [makeOption(placeholder[1])] : []; + } + }; + + Object.keys(city).forEach((code) => { + const option = makeOption(city[code], code, getCityChildren()); + cityMap.set(code.slice(0, 4), option); + + const province = provinceMap.get(code.slice(0, 2)); + if (province) { + province.children!.push(option); + } + }); + } + + if (showCounty) { + Object.keys(county).forEach((code) => { + const city = cityMap.get(code.slice(0, 4)); + if (city) { + city.children!.push(makeOption(county[code], code)); + } + }); + } + + const options = Array.from(provinceMap.values()); + + if (placeholder.length) { + const county = showCounty ? [makeOption(placeholder[2])] : undefined; + const city = showCity + ? [makeOption(placeholder[1], AREA_EMPTY_CODE, county)] + : undefined; + options.unshift(makeOption(placeholder[0], AREA_EMPTY_CODE, city)); + } + + return [options, provinceMap, cityMap] as const; +} diff --git a/packages/core/src/area-picker/area-picker.tsx b/packages/core/src/area-picker/area-picker.tsx index 77d9a2fd2..f79e46766 100644 --- a/packages/core/src/area-picker/area-picker.tsx +++ b/packages/core/src/area-picker/area-picker.tsx @@ -1,24 +1,145 @@ -import { AreaFormatter } from "@taroify/hooks/use-area" import * as React from "react" -import Picker, { MultiValuePickerProps } from "../picker" -import AreaPickerContext from "./area-picker.context" +import { useMemo, useRef, Children, ReactElement } from "react" +import { useUncontrolled } from "@taroify/hooks" +import Picker from "../picker" +import type { PickerBaseProps } from "../picker/picker" +import type { PickerOptionData, PickerOptionObject } from "../picker/picker.shared" +import { isElementOf } from "../utils/validate" +import { useMemoizedFn } from "../hooks" +import type { AreaData } from "./area-picker.shared" +import { formatDataForCascade } from "./area-picker.shared" +import AreaPickerColumns from "./area-picker-columns" -export interface AreaPickerProps extends MultiValuePickerProps { +export type AreaPickerProps = Omit & { depth?: number - formatter?: AreaFormatter + areaList?: AreaData + defaultValue?: string[] + value?: string[] + + onChange?(values: string[], option: PickerOptionObject, column: PickerOptionObject): void + + onConfirm?(values: string[], option: PickerOptionObject[]): void + + onCancel?(values: string[], option: PickerOptionObject[]): void } -function AreaPicker(props: AreaPickerProps) { - const { depth, formatter, ...restProps } = props +const defaultAreaList = {} + +function AreaPicker({ + areaList: areaListProp, + depth: depthProp, + children: childrenProps, + value: valueProp, + defaultValue, + onChange: onChangeProp, + onConfirm: onConfirmProp, + onCancel: onCancelProp, + ...restProps +}: AreaPickerProps) { + const hasChange = useRef(false) + const { value, setValue } = useUncontrolled({ + value: valueProp, + defaultValue + }) + const [areaList, columnsNum] = useMemo(() => { + let __areaList__ = areaListProp || defaultAreaList + Children.forEach(childrenProps, child => { + if (isElementOf(child, AreaPickerColumns)) { + __areaList__ = (child as ReactElement).props.children + } + }) + + return [__areaList__, depthProp || 3] + }, [childrenProps, areaListProp, depthProp]) + + const [origin, provinceMap, cityMap] = useMemo(() => formatDataForCascade({ + // @ts-ignore + areaList, + columnsNum + }), [areaList, columnsNum]) + + const columns = useMemo(() => { + const ret: PickerOptionData[] = [origin] + let parent = origin; + for (let i = 0; i < columnsNum - 1; i++) { + let selected: PickerOptionData | undefined + if (value?.[i]) { + selected = parent.find(item => item.value === value?.[i]) + } else { + selected = parent[0] + } + parent = selected?.children || [] + ret.push(parent) + } + + return ret + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [origin, value]) + const handleChange = useMemoizedFn((val, option, column) => { + const idx = val.findIndex(item => item === option.value) + let newVal = val + if (idx === 0) { + newVal = [val[0]] + if (columnsNum > 1) { + const province = provinceMap.get(option.value.slice(0, 2)) + const city = province?.children?.[0] + newVal.push(city?.value) + if (columnsNum > 2) { + newVal.push(city?.children?.[0]?.value) + } + } + } else if (idx === 1) { + const city = cityMap.get(option.value.slice(0, 4)) + newVal = [val[0], city?.value] + if (columnsNum > 2) { + newVal.push(city?.children?.[0]?.value) + } + } + hasChange.current = true + setValue(newVal) + onChangeProp?.(newVal, option, column) + }) + + const genValOption = (valueParam) => { + const val = (hasChange.current ? value : valueParam) || [] + const province = provinceMap.get(val[0].slice(0, 2)) || {} + const option = [province] + if (columnsNum > 1) { + // @ts-ignore + const { children, ...city } = cityMap.get(val[1].slice(0, 4)) || {} + option.push(city) + if (columnsNum > 2) { + const country = children?.find(item => item.value === val[2]) + option.push(country || {}) + } + } + return [val, option] + } + + const handleConfirm = useMemoizedFn((valueParam) => { + if (onConfirmProp) { + const [val, option] = genValOption(valueParam) + onConfirmProp(val, option) + } + }) + const handleCancel = useMemoizedFn((valueParam) => { + if (onCancelProp) { + const [val, option] = genValOption(valueParam) + onCancelProp(val, option) + } + }) + return ( - - - + ) } diff --git a/packages/core/src/picker/index.ts b/packages/core/src/picker/index.ts index 61e70a06e..37a21ed19 100644 --- a/packages/core/src/picker/index.ts +++ b/packages/core/src/picker/index.ts @@ -7,7 +7,7 @@ import PickerTitle from "./picker-title" import PickerToolbar from "./picker-toolbar" import { PickerColumn } from "./picker.composition" -export type { PickerProps, MultiValuePickerProps } from "./picker" +export type { PickerProps } from "./picker" export type { PickerOptionObject, PickerThemeVars } from "./picker.shared" export type { PickerColumnsProps } from "./picker-columns" export { default as PickerContext } from "./picker.context" diff --git a/packages/core/src/picker/picker.tsx b/packages/core/src/picker/picker.tsx index 559def333..ef27b0967 100644 --- a/packages/core/src/picker/picker.tsx +++ b/packages/core/src/picker/picker.tsx @@ -31,7 +31,7 @@ function usePickerValues(value?: any): any[] { return _.isArray(value) ? value : [value] } -interface PickerBaseProps extends ViewProps { +export interface PickerBaseProps extends ViewProps { readonly?: boolean loading?: boolean siblingCount?: number @@ -44,17 +44,6 @@ interface PickerBaseProps extends ViewProps { children?: ReactNode } -export interface MultiValuePickerProps extends PickerBaseProps { - defaultValue?: string[] - value?: string[] - - onChange?(values: string[], option: PickerOptionObject, column: PickerOptionObject): void - - onConfirm?(values: string[], options: PickerOptionObject[]): void - - onCancel?(values: string[], options: PickerOptionObject[]): void -} - export interface PickerProps extends PickerBaseProps { defaultValue?: string | string[] value?: string | string[] diff --git a/packages/demo/src/pages/form/area-picker/index.tsx b/packages/demo/src/pages/form/area-picker/index.tsx index 7ae743911..61f6df697 100644 --- a/packages/demo/src/pages/form/area-picker/index.tsx +++ b/packages/demo/src/pages/form/area-picker/index.tsx @@ -12,37 +12,22 @@ function toastOptions(options?: PickerOptionObject[]) { } function BasicAreaPicker() { - return ( - toastOptions(options)}> - - 取消 - 标题 - 确认 - - - - ) + return toastOptions(options)} areaList={areaList} /> } function AreaPickerWithValue() { + return toastOptions(options)} areaList={areaList} /> +} + +function AreaPickerWithColumns() { return ( - toastOptions(options)} - > - - 取消 - 标题 - 确认 - - - + toastOptions(options)} areaList={areaList} /> ) } -function AreaPickerWithColumns2() { +function ManualAreaPicker() { return ( - toastOptions(options)}> + toastOptions(options)}> 取消 标题 @@ -53,10 +38,10 @@ function AreaPickerWithColumns2() { ) } + export default function AreaPickerDemo() { return ( - @@ -64,8 +49,12 @@ export default function AreaPickerDemo() { - + + + + + ) } From 0e1eb08237d598c1352ebef03d4fb2b97d81f829 Mon Sep 17 00:00:00 2001 From: hbztd <38969912+hbztd@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:00:53 +0800 Subject: [PATCH 5/5] fix: AreaPicker renders a blank district column after directly modifying the district --- packages/core/src/area-picker/area-picker.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/area-picker/area-picker.tsx b/packages/core/src/area-picker/area-picker.tsx index f79e46766..e3fdbe9c1 100644 --- a/packages/core/src/area-picker/area-picker.tsx +++ b/packages/core/src/area-picker/area-picker.tsx @@ -77,7 +77,7 @@ function AreaPicker({ }, [origin, value]) const handleChange = useMemoizedFn((val, option, column) => { const idx = val.findIndex(item => item === option.value) - let newVal = val + let newVal if (idx === 0) { newVal = [val[0]] if (columnsNum > 1) { @@ -94,6 +94,8 @@ function AreaPicker({ if (columnsNum > 2) { newVal.push(city?.children?.[0]?.value) } + } else if (idx === 2) { + newVal = [value?.[0], value?.[1], option.value] } hasChange.current = true setValue(newVal)