diff --git a/packages/nutui-taro-demo/src/pages/index/index.tsx b/packages/nutui-taro-demo/src/pages/index/index.tsx index 00a634b55b..3c62071dce 100644 --- a/packages/nutui-taro-demo/src/pages/index/index.tsx +++ b/packages/nutui-taro-demo/src/pages/index/index.tsx @@ -82,7 +82,7 @@ const Index = () => { )} {nav.packages.map((com) => - com.show && com.taro && (harmony() ? com.dd : true) && (!search || new RegExp(search, 'ig').test(com.name.toLowerCase())) ? ( + com.show && com.taro && (!search || new RegExp(search, 'ig').test(com.name.toLowerCase())) ? (
请选择地址
  • 123
    探探鱼
    182****1718
    北京市次渠镇通州区
  • 123
    探探鱼
    182****1718
    钓鱼岛钓鱼岛全区
  • 456
    探探鱼
    182****1718
    北京市大兴区科创十一街18号院京东大厦
"`; -exports[`Address: show custom 1`] = `"
选择地址
请选择
浙江
湖南
福建
"`; +exports[`Address: show cascader false 1`] = `"
选择地址
请选择
浙江
湖南
福建
北京
"`; -exports[`Address: show custom 2`] = `"
选择地址
请选择
浙江
湖南
福建
"`; - -exports[`Address: show custom icon 1`] = `"
选择地址
请选择
浙江
湖南
福建
"`; +exports[`Address: show custom icon 1`] = `"
选择地址
请选择
浙江
湖南
福建
北京
"`; exports[`Address: show custom icon 2`] = `"
请选择地址
  • 探探鱼
    182****1718
    北京市次渠镇通州区
  • 探探鱼
    182****1718
    钓鱼岛钓鱼岛全区
  • 探探鱼
    182****1718
    北京市大兴区科创十一街18号院京东大厦
"`; +exports[`Address: show custom true 1`] = `"
选择地址
请选择
浙江
湖南
福建
北京
"`; + +exports[`Address: show elevator 1`] = `"
选择地址
热门城市
选择省份/地区
A
安徽
B
北京
C
重庆
F
福建
G
贵州
广东
广西
甘肃
H
河北
河南
湖南
湖北
S
山东
山西
上海
陕西
四川
Z
浙江
A
B
C
F
G
H
S
Z
"`; + exports[`Address: show exist 1`] = `"
选择地址
  • 探探鱼
    182****1718
    北京市次渠镇通州区
  • 探探鱼
    182****1718
    钓鱼岛钓鱼岛全区
  • 探探鱼
    182****1718
    北京市大兴区科创十一街18号院京东大厦
"`; diff --git a/src/packages/address/__test__/address.spec.tsx b/src/packages/address/__test__/address.spec.tsx index ae9806b843..41a6b1761c 100644 --- a/src/packages/address/__test__/address.spec.tsx +++ b/src/packages/address/__test__/address.spec.tsx @@ -102,9 +102,329 @@ const optionsDemo1 = [ }, ], }, + { + value: '北京', + text: '北京', + wordCode: 'B', + children: [ + { + value: '朝阳区', + text: '朝阳区', + wordCode: 'C', + }, + { + value: '昌平区', + text: '昌平区', + wordCode: 'C', + }, + { + value: '大兴区', + text: '大兴区', + wordCode: 'D', + }, + { + value: '东城区', + text: '东城区', + wordCode: 'D', + }, + { + value: '房山区', + text: '房山区', + wordCode: 'F', + }, + { + value: '丰台区', + text: '丰台区', + wordCode: 'F', + }, + ], + }, +] + +const hotList = [ + { name: '北京' }, + { name: '上海' }, + { name: '广州' }, + { name: '深圳' }, + { name: '杭州' }, + { name: '南京' }, + { name: '苏州' }, + { name: '天津' }, + { name: '武汉' }, + { name: '长沙' }, ] -test('Address: show custom', async () => { +const elevatorList = [ + { + value: '安徽', + text: '安徽', + wordCode: 'A', + children: [ + { + value: '安庆市', + text: '安庆市', + wordCode: 'A', + disabled: true, + children: [ + { + value: '大观区', + text: '大观区', + disabled: true, + wordCode: 'D', + }, + { value: '怀宁县', text: '怀宁县', wordCode: 'H' }, + { value: '岳西县', text: '岳西县', wordCode: 'Y' }, + { value: '迎江区', text: '迎江区', wordCode: 'Y' }, + { value: '宜秀区', text: '宜秀区', wordCode: 'Y' }, + ], + }, + { + value: '合肥市', + text: '合肥市', + wordCode: 'H', + children: [ + { value: '合肥高新', text: '合肥高新', wordCode: 'H' }, + { value: '合肥经济', text: '合肥经济', wordCode: 'H' }, + ], + }, + { + value: '淮北市', + text: '淮北市', + wordCode: 'H', + children: [ + { value: '杜集区', text: '杜集区', wordCode: 'D' }, + { value: '烈山区', text: '杜集区', wordCode: 'L' }, + ], + }, + { + value: '淮南市', + text: '淮南市', + wordCode: 'H', + children: [{ value: '八公山区', text: '八公山区', wordCode: 'B' }], + }, + { + value: '黄山市', + text: '黄山市', + wordCode: 'H', + children: [ + { value: '黄山区', text: '黄山区', wordCode: 'H' }, + { value: '徽州区', text: '徽州区', wordCode: 'H' }, + ], + }, + ], + }, + { + value: '北京', + text: '北京', + wordCode: 'B', + children: [ + { + value: '朝阳区', + text: '朝阳区', + wordCode: 'C', + }, + { + value: '昌平区', + text: '昌平区', + wordCode: 'C', + }, + { + value: '大兴区', + text: '大兴区', + wordCode: 'D', + }, + { + value: '东城区', + text: '东城区', + wordCode: 'D', + }, + { + value: '房山区', + text: '房山区', + wordCode: 'F', + }, + { + value: '丰台区', + text: '丰台区', + wordCode: 'F', + }, + ], + }, + { + value: '重庆', + text: '重庆', + wordCode: 'C', + }, + { + value: '福建', + text: '福建', + wordCode: 'F', + }, + { + value: '贵州', + text: '贵州', + wordCode: 'G', + }, + { + value: '广东', + text: '广东', + wordCode: 'G', + children: [ + { + value: '广州市', + text: '广州市', + wordCode: 'G', + children: [ + { + value: '白云区', + text: '白云区', + wordCode: 'B', + }, + { + value: '黄埔区', + text: '黄埔区', + wordCode: 'H', + }, + { + value: '花都区', + text: '花都区', + wordCode: 'H', + }, + { + value: '海珠区', + text: '海珠区', + wordCode: 'H', + }, + ], + }, + { + value: '深圳市', + text: '深圳市', + wordCode: 'S', + children: [ + { + value: '宝安区', + text: '宝安区', + wordCode: 'B', + }, + { + value: '罗湖区', + text: '罗湖区', + wordCode: 'L', + }, + { + value: '龙岗区', + text: '龙岗区', + wordCode: 'L', + }, + { + value: '龙华区', + text: '龙华区', + wordCode: 'L', + }, + ], + }, + ], + }, + { + value: '广西', + text: '广西', + wordCode: 'G', + }, + { + value: '甘肃', + text: '甘肃', + wordCode: 'G', + }, + { + value: '河北', + text: '河北', + wordCode: 'H', + disabled: true, + }, + { + value: '河南', + text: '河南', + wordCode: 'H', + disabled: true, + }, + { + value: '湖南', + text: '湖南', + wordCode: 'H', + disabled: true, + }, + { + value: '湖北', + text: '湖北', + wordCode: 'H', + disabled: true, + }, + { + value: '山东', + text: '山东', + wordCode: 'S', + }, + { + value: '山西', + text: '山西', + wordCode: 'S', + }, + { + value: '上海', + text: '上海', + wordCode: 'S', + children: [ + { + value: '宝山区', + text: '宝山区', + wordCode: 'B', + }, + { + value: '黄埔区', + text: '黄埔区', + wordCode: 'H', + }, + { + value: '虹口区', + text: '虹口区', + wordCode: 'H', + }, + { + value: '嘉定区', + text: '嘉定区', + wordCode: 'J', + }, + { + value: '静安区', + text: '静安区', + wordCode: 'J', + }, + { + value: '金山区', + text: '金山区', + wordCode: 'J', + }, + ], + }, + { + value: '陕西', + text: '陕西', + wordCode: 'S', + }, + { + value: '四川', + text: '四川', + wordCode: 'S', + }, + { + value: '浙江', + text: '浙江', + wordCode: 'Z', + }, +] + +test('Address: show cascader false', async () => { const { container } = render(
) @@ -118,13 +438,51 @@ test('Address: show custom', async () => { expect(container.innerHTML).toMatchSnapshot() }) -test('Address: show custom', async () => { +test('Address: show custom true', async () => { const { container } = render(
) expect(container.innerHTML).toMatchSnapshot() }) +test('Address: show elevator', async () => { + const { container } = render( +
+ ) + expect(container.innerHTML).toMatchSnapshot() + + const elevatorContainer = container.querySelectorAll( + '.nut-elevator-list-item-name' + ) + fireEvent.click(elevatorContainer[8]) + await waitFor(() => { + const selected = container.querySelector('.nut-address-selected-item') + expect(selected?.innerHTML).toBe('河北') + }) +}) + +test('Address: show elevator hotlist', async () => { + const { container } = render( +
+ ) + const hotitems = container.querySelectorAll('.nut-address-hotlist-item') + expect(hotitems.length).toBe(10) + expect(hotitems[0]).toHaveTextContent('北京') + fireEvent.click(hotitems[0]) + await waitFor(() => { + const hotitems = container.querySelectorAll('.nut-address-hotlist-item') + expect(hotitems.length).toBe(0) + const selected = container.querySelectorAll('.nut-address-selected-item') + expect(selected?.length).toBe(1) + }) +}) + test('Address: show custom icon', async () => { const { container } = render(
(type) const [innerVisible, setInnerVisible] = usePropsValue({ @@ -96,19 +97,16 @@ const InternalAddress: ForwardRefRenderFunction< onClose && onClose() } - const renderLeftOnCustomSwitch = () => { + const renderLeftOnCascaderSwitch = () => { + if (!custom) return null return ( - <> - {custom && ( - - {React.isValidElement(backIcon) ? ( - backIcon - ) : ( - - )} - + + {React.isValidElement(backIcon) ? ( + backIcon + ) : ( + )} - + ) } @@ -118,62 +116,89 @@ const InternalAddress: ForwardRefRenderFunction< } const onSwitchModule = () => { - if (currentType === 'exist') { - setCurrentType('custom') - } else { - setCurrentType('exist') - } - onSwitch && onSwitch({ type: currentType }) + const nextType = currentType === 'exist' ? 'cascader' : 'exist' + setCurrentType(nextType) + onSwitch && onSwitch({ type: nextType }) + } + const renderElevator = () => { + return ( + { + onChange?.(val, params) + }} + /> + ) + } + const renderCascator = () => { + return ( + { + onChange && onChange(val, params) + }} + /> + ) + } + const renderExist = () => { + return ( + + + { + // 不需要 close,选中切换即关闭弹框。可手动关闭弹框,只关闭弹框不处理逻辑。 + + } + + + ) } return ( <> - {currentType === 'custom' || currentType === 'custom2' ? ( - { - onChange?.(val, params) - }} - /> - ) : ( - - - { - // 不需要 close,选中切换即关闭弹框。可手动关闭弹框,只关闭弹框不处理逻辑。 - - } - - - )} + {currentType === 'elevator' ? renderElevator() : null} + {currentType === 'cascader' ? renderCascator() : null} + {currentType === 'exist' ? renderExist() : null} ) } diff --git a/src/packages/address/address.tsx b/src/packages/address/address.tsx index 08a8e90e73..7cac43e1bf 100644 --- a/src/packages/address/address.tsx +++ b/src/packages/address/address.tsx @@ -6,7 +6,8 @@ import React, { } from 'react' import { ArrowLeft } from '@nutui/icons-react' import Popup from '@/packages/popup' -import { CustomRender } from './customRender' +import { CascaderRender } from './cascaderRender' +import { ElevatorRender } from './elevatorRender' import { ExistRender } from './existRender' import { useConfig } from '@/packages/configprovider' import { @@ -17,16 +18,18 @@ import { } from '@/types' import { ComponentDefaults } from '@/utils/typings' import { usePropsValue } from '@/hooks/use-props-value' +import { mergeProps } from '@/utils/merge-props' const defaultProps = { ...ComponentDefaults, defaultValue: [], - type: 'custom', + type: 'cascader', options: [], optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, format: {}, custom: false, existList: [], + hotList: [], height: '200px', defaultIcon: null, selectIcon: null, @@ -62,15 +65,13 @@ export const InternalAddress: ForwardRefRenderFunction< defaultIcon, closeIcon, backIcon, + hotList, onChange, onExistSelect, onClose, onSwitch, ...rest - } = { - ...defaultProps, - ...props, - } + } = mergeProps(defaultProps, props) const classPrefix = 'nut-address' const [currentType, setCurrentType] = useState(type) const [innerVisible, setInnerVisible] = usePropsValue({ @@ -95,19 +96,17 @@ export const InternalAddress: ForwardRefRenderFunction< onClose && onClose() } - const renderLeftOnCustomSwitch = () => { - if (custom) { - return ( -
- {React.isValidElement(backIcon) ? ( - backIcon - ) : ( - - )} -
- ) - } - return null + const renderLeftOnCascaderSwitch = () => { + if (!custom) return null + return ( +
+ {React.isValidElement(backIcon) ? ( + backIcon + ) : ( + + )} +
+ ) } const selectedExistItem = (data: AddressList) => { @@ -116,62 +115,89 @@ export const InternalAddress: ForwardRefRenderFunction< } const onSwitchModule = () => { - if (currentType === 'exist') { - setCurrentType('custom') - } else { - setCurrentType('exist') - } - onSwitch && onSwitch({ type: currentType }) + const nextType = currentType === 'exist' ? 'cascader' : 'exist' + setCurrentType(nextType) + onSwitch && onSwitch({ type: nextType }) + } + const renderElevator = () => { + return ( + { + onChange?.(val, params) + }} + /> + ) + } + const renderCascator = () => { + return ( + { + onChange && onChange(val, params) + }} + /> + ) + } + const renderExist = () => { + return ( + +
+ { + // 不需要 close,选中切换即关闭弹框。可手动关闭弹框,只关闭弹框不处理逻辑。 + + } +
+
+ ) } return ( <> - {currentType === 'custom' || currentType === 'custom2' ? ( - { - onChange?.(val, params) - }} - /> - ) : ( - -
- { - // 不需要 close,选中切换即关闭弹框。可手动关闭弹框,只关闭弹框不处理逻辑。 - - } -
-
- )} + {currentType === 'elevator' ? renderElevator() : null} + {currentType === 'cascader' ? renderCascator() : null} + {currentType === 'exist' ? renderExist() : null} ) } diff --git a/src/packages/address/customRender.taro.tsx b/src/packages/address/cascaderRender.taro.tsx similarity index 94% rename from src/packages/address/customRender.taro.tsx rename to src/packages/address/cascaderRender.taro.tsx index ab2fd80289..797a93932c 100644 --- a/src/packages/address/customRender.taro.tsx +++ b/src/packages/address/cascaderRender.taro.tsx @@ -22,14 +22,14 @@ export interface AddressProps extends TaroCascaderProps { const defaultProps = { ...ComponentDefaults, visible: false, - type: 'custom', + type: 'cascader', options: [], optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, format: {}, height: '200px', } as unknown as AddressProps -export const CustomRender: FunctionComponent< +export const CascaderRender: FunctionComponent< Partial & Omit< React.HTMLAttributes, @@ -59,7 +59,7 @@ export const CustomRender: FunctionComponent< return ( <> - {type === 'custom' && ( + {type === 'cascader' && ( & Omit< React.HTMLAttributes, @@ -59,7 +59,7 @@ export const CustomRender: FunctionComponent< return ( <> - {type === 'custom' && ( + {type === 'cascader' && ( { const [translated] = useTranslate({ 'zh-CN': { - customAddress: '选择自定义地址', + elevator: '电梯方式的地址展示', + cascaderAddress: '级联方式的地址展示', selectCity: '选中省市区', existList: '选择已有地址', customIcon: '自定义图标', @@ -21,7 +23,8 @@ const AddressDemo = () => { uncontrolled: '非受控方式', }, 'zh-TW': { - customAddress: '選擇自定義地址', + elevator: '電梯方式的地址展示', + cascaderAddress: '級聯方式的地址展示', selectCity: '選中省市區', existList: '選擇已有地址', customIcon: '自定義圖標', @@ -29,7 +32,8 @@ const AddressDemo = () => { uncontrolled: '非受控方式', }, 'en-US': { - customAddress: 'Choose Custom Address', + elevator: 'Elevator Address', + cascaderAddress: 'Choose Cascader Address', selectCity: 'Choose City', existList: 'Choose Exist Address', customIcon: 'Custom Icon', @@ -42,6 +46,8 @@ const AddressDemo = () => { <>
+ {translated.elevator} + {translated.customAddress} {translated.selectCity} diff --git a/src/packages/address/demo.tsx b/src/packages/address/demo.tsx index 79f6745e77..a775a40e5a 100644 --- a/src/packages/address/demo.tsx +++ b/src/packages/address/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 Demo11 from './demos/h5/demo11' const AddressDemo = () => { const [translated] = useTranslate({ 'zh-CN': { - customAddress: '选择自定义地址', + elevator: '电梯方式的地址展示', + cascaderAddress: '级联方式的地址展示', selectCity: '选中省市区', existList: '选择已有地址', customIcon: '自定义图标', @@ -18,7 +20,8 @@ const AddressDemo = () => { uncontrolled: '非受控方式', }, 'zh-TW': { - customAddress: '選擇自定義地址', + elevator: '電梯方式的地址展示', + cascaderAddress: '級聯方式的地址展示', selectCity: '選中省市區', existList: '選擇已有地址', customIcon: '自定義圖標', @@ -26,7 +29,8 @@ const AddressDemo = () => { uncontrolled: '非受控方式', }, 'en-US': { - customAddress: 'Choose Custom Address', + elevator: 'Elevator Address', + cascaderAddress: 'Choose Cascader Address', selectCity: 'Choose City', existList: 'Choose Exist Address', customIcon: 'Custom Icon', @@ -38,7 +42,9 @@ const AddressDemo = () => { return ( <>
-

{translated.customAddress}

+

{translated.elevator}

+ +

{translated.cascaderAddress}

{translated.selectCity}

diff --git a/src/packages/address/demos/h5/demo1.tsx b/src/packages/address/demos/h5/demo1.tsx index 945b80217e..bb477a7ee9 100644 --- a/src/packages/address/demos/h5/demo1.tsx +++ b/src/packages/address/demos/h5/demo1.tsx @@ -2,8 +2,8 @@ import React, { useState } from 'react' import { Address, Cell } from '@nutui/nutui-react' const Demo1 = () => { - const [text, setText] = useState('选择地址') - const [optionsDemo1] = useState([ + const [text, setText] = useState('请选择地址') + const [optionsDemo] = useState([ { value: '浙江', text: '浙江', @@ -71,15 +71,15 @@ const Demo1 = () => { return ( <> setVisible(true)} />
{ + onChange={(value, params) => { setText(value.join('')) }} onClose={() => setVisible(false)} diff --git a/src/packages/address/demos/h5/demo11.tsx b/src/packages/address/demos/h5/demo11.tsx new file mode 100644 index 0000000000..ec871d4783 --- /dev/null +++ b/src/packages/address/demos/h5/demo11.tsx @@ -0,0 +1,314 @@ +import React, { useEffect, useState } from 'react' +import { Address, Cell, CascaderOption } from '@nutui/nutui-react' + +const Demo1 = () => { + const [text, setText] = useState('选择地址') + const [hotList] = useState([ + { name: '北京' }, + { name: '上海' }, + { name: '广州' }, + { name: '深圳' }, + { name: '杭州' }, + { name: '南京' }, + { name: '苏州' }, + { name: '天津' }, + { name: '武汉' }, + { name: '长沙' }, + ]) + const [options, setOptions] = useState([]) + useEffect(() => { + setOptions([ + { + value: '安徽', + text: '安徽', + wordCode: 'A', + children: [ + { + value: '安庆市', + text: '安庆市', + wordCode: 'A', + disabled: true, + children: [ + { + value: '大观区', + text: '大观区', + disabled: true, + wordCode: 'D', + }, + { value: '怀宁县', text: '怀宁县', wordCode: 'H' }, + { value: '岳西县', text: '岳西县', wordCode: 'Y' }, + { value: '迎江区', text: '迎江区', wordCode: 'Y' }, + { value: '宜秀区', text: '宜秀区', wordCode: 'Y' }, + ], + }, + { + value: '合肥市', + text: '合肥市', + wordCode: 'H', + children: [ + { value: '合肥高新', text: '合肥高新', wordCode: 'H' }, + { value: '合肥经济', text: '合肥经济', wordCode: 'H' }, + ], + }, + { + value: '淮北市', + text: '淮北市', + wordCode: 'H', + children: [ + { value: '杜集区', text: '杜集区', wordCode: 'D' }, + { value: '烈山区', text: '烈山区', wordCode: 'L' }, + ], + }, + { + value: '淮南市', + text: '淮南市', + wordCode: 'H', + children: [{ value: '八公山区', text: '八公山区', wordCode: 'B' }], + }, + { + value: '黄山市', + text: '黄山市', + wordCode: 'H', + children: [ + { value: '黄山区', text: '黄山区', wordCode: 'H' }, + { value: '徽州区', text: '徽州区', wordCode: 'H' }, + ], + }, + ], + }, + { + value: '北京', + text: '北京', + wordCode: 'B', + children: [ + { + value: '朝阳区', + text: '朝阳区', + wordCode: 'C', + }, + { + value: '昌平区', + text: '昌平区', + wordCode: 'C', + }, + { + value: '大兴区', + text: '大兴区', + wordCode: 'D', + }, + { + value: '东城区', + text: '东城区', + wordCode: 'D', + }, + { + value: '房山区', + text: '房山区', + wordCode: 'F', + }, + { + value: '丰台区', + text: '丰台区', + wordCode: 'F', + }, + ], + }, + { + value: '重庆', + text: '重庆', + wordCode: 'C', + }, + { + value: '福建', + text: '福建', + wordCode: 'F', + }, + { + value: '贵州', + text: '贵州', + wordCode: 'G', + }, + { + value: '广东', + text: '广东', + wordCode: 'G', + children: [ + { + value: '广州市', + text: '广州市', + wordCode: 'G', + children: [ + { + value: '白云区', + text: '白云区', + wordCode: 'B', + }, + { + value: '黄埔区', + text: '黄埔区', + wordCode: 'H', + }, + { + value: '花都区', + text: '花都区', + wordCode: 'H', + }, + { + value: '海珠区', + text: '海珠区', + wordCode: 'H', + }, + ], + }, + { + value: '深圳市', + text: '深圳市', + wordCode: 'S', + children: [ + { + value: '宝安区', + text: '宝安区', + wordCode: 'B', + }, + { + value: '罗湖区', + text: '罗湖区', + wordCode: 'L', + }, + { + value: '龙岗区', + text: '龙岗区', + wordCode: 'L', + }, + { + value: '龙华区', + text: '龙华区', + wordCode: 'L', + }, + ], + }, + ], + }, + { + value: '广西', + text: '广西', + wordCode: 'G', + }, + { + value: '甘肃', + text: '甘肃', + wordCode: 'G', + }, + { + value: '河北', + text: '河北', + wordCode: 'H', + disabled: true, + }, + { + value: '河南', + text: '河南', + wordCode: 'H', + disabled: true, + }, + { + value: '湖南', + text: '湖南', + wordCode: 'H', + disabled: true, + }, + { + value: '湖北', + text: '湖北', + wordCode: 'H', + disabled: true, + }, + { + value: '山东', + text: '山东', + wordCode: 'S', + }, + { + value: '山西', + text: '山西', + wordCode: 'S', + }, + { + value: '上海', + text: '上海', + wordCode: 'S', + children: [ + { + value: '宝山区', + text: '宝山区', + wordCode: 'B', + }, + { + value: '黄埔区', + text: '黄埔区', + wordCode: 'H', + }, + { + value: '虹口区', + text: '虹口区', + wordCode: 'H', + }, + { + value: '嘉定区', + text: '嘉定区', + wordCode: 'J', + }, + { + value: '静安区', + text: '静安区', + wordCode: 'J', + }, + { + value: '金山区', + text: '金山区', + wordCode: 'J', + }, + ], + }, + { + value: '陕西', + text: '陕西', + wordCode: 'S', + }, + { + value: '四川', + text: '四川', + wordCode: 'S', + }, + { + value: '浙江', + text: '浙江', + wordCode: 'Z', + }, + ]) + }, []) + const [visible, setVisible] = useState(false) + + return ( + <> + setVisible(true)} + /> +
{ + console.log('value', value) + setText(value.join('')) + }} + onClose={() => setVisible(false)} + /> + + ) +} +export default Demo1 diff --git a/src/packages/address/demos/h5/demo5.tsx b/src/packages/address/demos/h5/demo5.tsx index 158b0320b6..1f9b7cad6a 100644 --- a/src/packages/address/demos/h5/demo5.tsx +++ b/src/packages/address/demos/h5/demo5.tsx @@ -93,7 +93,7 @@ const Demo5 = () => { } const onSwitch = (val: { type: string }) => { - if (val.type === 'custom') { + if (val.type === 'cascader') { console.log('点击了“选择其他地址”按钮') } else { console.log('点击了自定义地址左上角的返回按钮') diff --git a/src/packages/address/demos/taro/demo11.tsx b/src/packages/address/demos/taro/demo11.tsx new file mode 100644 index 0000000000..5417c5a1ed --- /dev/null +++ b/src/packages/address/demos/taro/demo11.tsx @@ -0,0 +1,313 @@ +import React, { useEffect, useState } from 'react' +import { Address, Cell, CascaderOption } from '@nutui/nutui-react-taro' + +const Demo1 = () => { + const [text, setText] = useState('选择地址') + const [hotList] = useState([ + { name: '北京' }, + { name: '上海' }, + { name: '广州' }, + { name: '深圳' }, + { name: '杭州' }, + { name: '南京' }, + { name: '苏州' }, + { name: '天津' }, + { name: '武汉' }, + { name: '长沙' }, + ]) + const [options, setOptions] = useState([]) + useEffect(() => { + setOptions([ + { + value: '安徽', + text: '安徽', + wordCode: 'A', + children: [ + { + value: '安庆市', + text: '安庆市', + wordCode: 'A', + disabled: true, + children: [ + { + value: '大观区', + text: '大观区', + disabled: true, + wordCode: 'D', + }, + { value: '怀宁县', text: '怀宁县', wordCode: 'H' }, + { value: '岳西县', text: '岳西县', wordCode: 'Y' }, + { value: '迎江区', text: '迎江区', wordCode: 'Y' }, + { value: '宜秀区', text: '宜秀区', wordCode: 'Y' }, + ], + }, + { + value: '合肥市', + text: '合肥市', + wordCode: 'H', + children: [ + { value: '合肥高新', text: '合肥高新', wordCode: 'H' }, + { value: '合肥经济', text: '合肥经济', wordCode: 'H' }, + ], + }, + { + value: '淮北市', + text: '淮北市', + wordCode: 'H', + children: [ + { value: '杜集区', text: '杜集区', wordCode: 'D' }, + { value: '烈山区', text: '烈山区', wordCode: 'L' }, + ], + }, + { + value: '淮南市', + text: '淮南市', + wordCode: 'H', + children: [{ value: '八公山区', text: '八公山区', wordCode: 'B' }], + }, + { + value: '黄山市', + text: '黄山市', + wordCode: 'H', + children: [ + { value: '黄山区', text: '黄山区', wordCode: 'H' }, + { value: '徽州区', text: '徽州区', wordCode: 'H' }, + ], + }, + ], + }, + { + value: '北京', + text: '北京', + wordCode: 'B', + children: [ + { + value: '朝阳区', + text: '朝阳区', + wordCode: 'C', + }, + { + value: '昌平区', + text: '昌平区', + wordCode: 'C', + }, + { + value: '大兴区', + text: '大兴区', + wordCode: 'D', + }, + { + value: '东城区', + text: '东城区', + wordCode: 'D', + }, + { + value: '房山区', + text: '房山区', + wordCode: 'F', + }, + { + value: '丰台区', + text: '丰台区', + wordCode: 'F', + }, + ], + }, + { + value: '重庆', + text: '重庆', + wordCode: 'C', + }, + { + value: '福建', + text: '福建', + wordCode: 'F', + }, + { + value: '贵州', + text: '贵州', + wordCode: 'G', + }, + { + value: '广东', + text: '广东', + wordCode: 'G', + children: [ + { + value: '广州市', + text: '广州市', + wordCode: 'G', + children: [ + { + value: '白云区', + text: '白云区', + wordCode: 'B', + }, + { + value: '黄埔区', + text: '黄埔区', + wordCode: 'H', + }, + { + value: '花都区', + text: '花都区', + wordCode: 'H', + }, + { + value: '海珠区', + text: '海珠区', + wordCode: 'H', + }, + ], + }, + { + value: '深圳市', + text: '深圳市', + wordCode: 'S', + children: [ + { + value: '宝安区', + text: '宝安区', + wordCode: 'B', + }, + { + value: '罗湖区', + text: '罗湖区', + wordCode: 'L', + }, + { + value: '龙岗区', + text: '龙岗区', + wordCode: 'L', + }, + { + value: '龙华区', + text: '龙华区', + wordCode: 'L', + }, + ], + }, + ], + }, + { + value: '广西', + text: '广西', + wordCode: 'G', + }, + { + value: '甘肃', + text: '甘肃', + wordCode: 'G', + }, + { + value: '河北', + text: '河北', + wordCode: 'H', + disabled: true, + }, + { + value: '河南', + text: '河南', + wordCode: 'H', + disabled: true, + }, + { + value: '湖南', + text: '湖南', + wordCode: 'H', + disabled: true, + }, + { + value: '湖北', + text: '湖北', + wordCode: 'H', + disabled: true, + }, + { + value: '山东', + text: '山东', + wordCode: 'S', + }, + { + value: '山西', + text: '山西', + wordCode: 'S', + }, + { + value: '上海', + text: '上海', + wordCode: 'S', + children: [ + { + value: '宝山区', + text: '宝山区', + wordCode: 'B', + }, + { + value: '黄埔区', + text: '黄埔区', + wordCode: 'H', + }, + { + value: '虹口区', + text: '虹口区', + wordCode: 'H', + }, + { + value: '嘉定区', + text: '嘉定区', + wordCode: 'J', + }, + { + value: '静安区', + text: '静安区', + wordCode: 'J', + }, + { + value: '金山区', + text: '金山区', + wordCode: 'J', + }, + ], + }, + { + value: '陕西', + text: '陕西', + wordCode: 'S', + }, + { + value: '四川', + text: '四川', + wordCode: 'S', + }, + { + value: '浙江', + text: '浙江', + wordCode: 'Z', + }, + ]) + }, []) + const [visible, setVisible] = useState(false) + + return ( + <> + setVisible(true)} + /> +
{ + setText(value.join('')) + }} + onClose={() => setVisible(false)} + /> + + ) +} +export default Demo1 diff --git a/src/packages/address/doc.en-US.md b/src/packages/address/doc.en-US.md index 9f8e235685..aa76722e52 100644 --- a/src/packages/address/doc.en-US.md +++ b/src/packages/address/doc.en-US.md @@ -10,6 +10,14 @@ import { Address } from '@nutui/nutui-react' ## Demo +### Elevator Address + +:::demo + + + +::: + ### Choose Custom Address :::demo @@ -66,7 +74,7 @@ If you want to select a province, you need to set the region ID in the order of | --- | --- | --- | --- | | visible | Whether to open address | `boolean` | `-` | | defaultVisible | Initial open/close state of the address selection | `boolean` | - | -| type | Choose type: exist/custom | `string` | `custom` | +| type | Choose type: exist/cascader/elevator | `AddressModeType` | `cascader` | | existList | Exist address list data | `Array` | `[]` | | defaultIcon | Exist address default icon | `ReactNode` | `-` | | selectIcon | Exist address selected icon | `ReactNode` | `-` | diff --git a/src/packages/address/doc.md b/src/packages/address/doc.md index 46066dfd34..bce194de03 100644 --- a/src/packages/address/doc.md +++ b/src/packages/address/doc.md @@ -1,6 +1,6 @@ # Address 地址 -地址选择,在2.0中,地址改用级联组件实现。截止当前版本,只支持级联,不支持级联+电梯模式,开发中。 +地址选择,在2.0中,地址改用级联组件实现。新增 电梯模式,支持热门城市的快速定位。 ## 引入 @@ -10,6 +10,14 @@ import { Address } from '@nutui/nutui-react' ## 示例代码 +### 电梯地址 + +:::demo + + + +::: + ### 选择自定义地址 :::demo @@ -68,13 +76,14 @@ import { Address } from '@nutui/nutui-react' | --- | --- | --- | --- | | visible | 是否打开地址选择 | `boolean` | `-` | | defaultVisible | 初始地址选择打开/关闭状态 | `boolean` | `-` | -| type | 地址选择类型 exist/custom | `string` | `custom` | +| type | 地址选择类型 exist/cascader/elevator | `AddressModeType` | `cascader` | | existList | 已存在地址列表,每个地址对象中,必传值provinceName、cityName、countyName、townName、addressDetail、selectedAddress(字段解释见下) | `Array` | `[]` | | defaultIcon | 已有地址列表默认图标,type='exist' 时生效 | `ReactNode` | `-` | | selectIcon | 已有地址列表选中图标,type='exist' 时生效 | `ReactNode` | `-` | | closeIcon | 自定义关闭弹框按钮图标 | `ReactNode` | `-` | | backIcon | 自定义地址与已有地址切换时,自定义返回的按钮图标 | `ReactNode` | `-` | | custom | 是否可以切换自定义地址选择,type='exist' 时生效 | `boolean` \| `string` | `true` | +| hotList | 是否展示热门城市,目前只在电梯模式下有效 | `Array` | - | | onExistSelect | 选择已有地址列表时触发 | `(data: AddressList) => void` | `-` | | onSwitch | 点击'选择其他地址'或自定义地址选择左上角返回按钮触发 | `(data: { type: string }) => void` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | diff --git a/src/packages/address/doc.taro.md b/src/packages/address/doc.taro.md index 4ed4987c09..b7ebe2daa8 100644 --- a/src/packages/address/doc.taro.md +++ b/src/packages/address/doc.taro.md @@ -10,6 +10,14 @@ import { Address } from '@nutui/nutui-react-taro' ## 示例代码 +### 电梯地址 + +:::demo + + + +::: + ### 选择自定义地址 :::demo @@ -68,13 +76,14 @@ import { Address } from '@nutui/nutui-react-taro' | --- | --- | --- | --- | | visible | 是否打开地址选择 | `boolean` | `-` | | defaultVisible | 初始地址选择打开/关闭状态 | `boolean` | `-` | -| type | 地址选择类型 exist/custom | `string` | `custom` | +| type | 地址选择类型 exist/cascader/elevator | `AddressModeType` | `cascader` | | existList | 已存在地址列表,每个地址对象中,必传值provinceName、cityName、countyName、townName、addressDetail、selectedAddress(字段解释见下) | `Array` | `[]` | | defaultIcon | 已有地址列表默认图标,type='exist' 时生效 | `ReactNode` | `-` | | selectIcon | 已有地址列表选中图标,type='exist' 时生效 | `ReactNode` | `-` | | closeIcon | 自定义关闭弹框按钮图标 | `ReactNode` | `-` | | backIcon | 自定义地址与已有地址切换时,自定义返回的按钮图标 | `ReactNode` | `-` | | custom | 是否可以切换自定义地址选择,type='exist' 时生效 | `boolean` \| `string` | `true` | +| hotList | 是否展示热门城市,目前只在电梯模式下有效 | `Array` | - | | onExistSelect | 选择已有地址列表时触发 | `(data: AddressList) => void` | `-` | | onSwitch | 点击'选择其他地址'或自定义地址选择左上角返回按钮触发 | `(data: { type: string }) => void` | `-` | | onClose | 关闭弹框时触发 | `-` | `-` | diff --git a/src/packages/address/doc.zh-TW.md b/src/packages/address/doc.zh-TW.md index 0559366db6..0170788942 100644 --- a/src/packages/address/doc.zh-TW.md +++ b/src/packages/address/doc.zh-TW.md @@ -10,6 +10,14 @@ import { Address } from '@nutui/nutui-react' ## 示例代碼 +### 电梯地址 + +:::demo + + + +::: + ### 選擇自定義地址 :::demo @@ -60,7 +68,7 @@ import { Address } from '@nutui/nutui-react' | --- | --- | --- | --- | | visible | 是否打開地址選擇 | `boolean` | `-` | | defaultVisible | 初始地址選擇打開/關閉狀態 | `boolean` | - | -| type | 地址選擇類型 exist/custom | `string` | `custom` | +| type | 地址選擇類型 exist/cascader/elevator | `AddressModeType` | `cascader` | | existList | 已存在地址列錶,每個地址對象中,必傳值provinceName、cityName、countyName、townName、addressDetail、selectedAddress(字段解釋見下) | `Array` | `[]` | | defaultIcon | 已有地址列錶默認圖標,type='exist' 時生效 | `ReactNode` | `-` | | selectIcon | 已有地址列錶選中圖標,type='exist' 時生效 | `ReactNode` | `-` | diff --git a/src/packages/address/elevator.scss b/src/packages/address/elevator.scss new file mode 100644 index 0000000000..6a8f3c3005 --- /dev/null +++ b/src/packages/address/elevator.scss @@ -0,0 +1,116 @@ +@import '../elevator/elevator.scss'; +@import '../cascader/cascader.scss'; + +.nut-address { + &-title { + font-size: 14px; + font-weight: 500; + padding: 16px 16px 12px 16px; + } + &-hotlist { + padding: 0 16px; + display: flex; + flex-wrap: wrap; + align-items: flex-start; + &-item { + display: flex; + justify-content: center; + align-items: center; + width: 63px; + height: 28px; + font-size: 12px; + border-radius: 4px; + margin-bottom: 7px; + /* #ifdef harmony*/ + margin-right: 6px; + /* #endif */ + /* #ifndef harmony*/ + margin-right: 7px; + /* #endif */ + background-color: $color-background-sunken; + color: $color-title; + &:nth-child(5n) { + margin-right: 0; + } + } + + &.hotlist-more { + .nut-address-hotlist-item { + width: auto; + padding: 0 16px; + margin-right: 7px; + } + } + } + + &-selected { + width: 100%; + height: 60px; + padding: 0 16px; + display: flex; + align-items: center; + border-bottom: 1px solid $color-border; + &-item { + font-size: 12px; + display: inline-block; + height: 28px; + line-height: 28px; + padding: 0 12px; + border-radius: 4px; + background-color: $color-background-sunken; + + &.active { + border: 1px solid $color-primary; + background-color: $color-primary-light-pressed; + color: $color-primary; + } + } + &-border { + margin: 0 2px; + color: $color-text-disabled; + } + } + + &-elevator { + margin-top: 0; + .nut-elevator-list { + &-item { + position: relative; + padding-left: 20px; + } + &-item-code { + display: inline; + position: absolute; + left: 0; + top: 0; + height: 30px; + line-height: 30px; + border-bottom: 0; + color: $color-text-help; + font-weight: 500; + } + } + .nut-elevator-bars { + top: 40%; + padding: 0; + background: none; + + &-inner-item { + display: flex; + justify-content: center; + align-items: center; + width: 16px; + height: 16px; + font-size: 10px; + border-radius: 16px; + margin-bottom: 2px; + color: $color-text-help; + &-active { + background-color: $color-primary; + color: $color-background-overlay; + font-weight: 400; + } + } + } + } +} diff --git a/src/packages/address/elevatorRender.taro.tsx b/src/packages/address/elevatorRender.taro.tsx new file mode 100644 index 0000000000..373c3ac020 --- /dev/null +++ b/src/packages/address/elevatorRender.taro.tsx @@ -0,0 +1,311 @@ +import React, { FunctionComponent, useEffect, useMemo, useState } from 'react' + +import { View } from '@tarojs/components' +import Popup from '@/packages/popup/index.taro' +import Elevator from '@/packages/elevator/index.taro' +import { + normalizeListOptions, + normalizeOptions, +} from '@/packages/cascader/utils' +import { transformData, findDataByName } from './utils' +import { + CascaderOption, + TaroCascaderProps, + CascaderValue, + CascaderOptionKey, + RegionData, +} from '@/types' +import { ComponentDefaults } from '@/utils/typings' +import { mergeProps } from '@/utils/merge-props' +import { usePropsValue } from '@/hooks/use-props-value' +import { isEmpty } from '@/utils/is-empty' +import { useConfig } from '@/packages/configprovider/index.taro' + +export interface AddressProps extends TaroCascaderProps { + visible: boolean // popup visible + type: string + options: CascaderOption[] + hotList: RegionData[] + value: CascaderValue + defaultValue: CascaderValue + optionKey: CascaderOptionKey + format: Record + height: string | number +} + +const defaultProps = { + ...ComponentDefaults, + visible: false, + type: 'elevator', + options: [], + optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + format: {}, + height: '200px', + activeColor: '', + activeIcon: 'checklist', + popup: true, + closeable: false, + closeIconPosition: 'top-right', + closeIcon: 'close', + lazy: false, + hotList: [], + onClose: () => {}, + onChange: () => {}, + onPathChange: () => {}, +} + +export const ElevatorRender: FunctionComponent< + Partial & + Omit< + React.HTMLAttributes, + 'title' | 'defaultValue' | 'onChange' + > +> = (props) => { + const { + hotList, + title, + left, + optionKey, + format, + onClose, + onChange, + onPathChange, + popup, + popupProps = {}, + visible: outerVisible, + options: outerOptions, + value: outerValue, + defaultValue: outerDefaultValue, + closeable, + closeIconPosition, + closeIcon, + } = mergeProps(defaultProps, props) + const { + locale: { + select, + address: { hotCity, selectProvince }, + }, + } = useConfig() + const classPrefix = 'nut-address' + const MAX_LENGTH = 10 + + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + onChange(value, []) + onPathChange(value, []) + }, + }) + + const [innerOptions, setInnerOptions] = useState(outerOptions) + const [innerValue, setInnerValue] = useState(value) + const [elevatorOptions, setElevatorOptions] = useState([]) + const [addressTip, setAddressTip] = useState(selectProvince) + const [levelIndex, setLevelIndex] = useState(0) + const [tabActiveIndex, setTabActiveIndex] = useState(0) + + // 初始化数据,只格式化一次;动态数据todo + const options = useMemo(() => { + if (!isEmpty(format)) { + return transformData(normalizeListOptions(innerOptions, format)) + } + if (!isEmpty(optionKey)) { + return transformData(normalizeOptions(innerOptions, optionKey) || []) + } + return transformData(innerOptions) + }, [innerOptions, optionKey, format]) + + useEffect(() => { + setElevatorOptions(options) + }, [options]) + + const levels = useMemo(() => { + const next = [] + let end = false + let currentOptions = options + innerValue.forEach((val, index) => { + const opt = currentOptions + ?.flatMap((o: any) => o.list.find((item: any) => item.name === val)) + .filter((item) => item !== undefined)[0] + + next.push({ + name: val, + children: currentOptions, + levelIndex: index, + current: index === tabActiveIndex, + }) + if (opt?.children) { + currentOptions = opt.children + } else { + end = true + } + }) + if (!end) { + next.push({ + name: null, + children: currentOptions, + levels: -1, + current: false, + }) + } + return next + }, [innerValue, options, tabActiveIndex]) + + const [visible, setVisible] = usePropsValue({ + value: outerVisible, + defaultValue: undefined, + onChange: (value) => { + if (value === false) onClose() + }, + }) + + useEffect(() => { + if (!visible) setInnerValue(value) + }, [visible, value]) + + useEffect(() => { + setInnerOptions(outerOptions) + }, [outerOptions]) + + useEffect(() => { + setTabActiveIndex(levels.length - 1) + setAddressTip(innerValue.length ? select : selectProvince) + }, [innerValue]) + + const handleElevatorItemClick = ( + elevatorItem: RegionData, + levelIndex: number + ) => { + const nextValue = innerValue.slice(0, levelIndex) + if (elevatorItem.name) { + nextValue[levelIndex] = elevatorItem.name + } + setInnerValue(nextValue) + if (elevatorItem.children?.length) { + setElevatorOptions(elevatorItem.children) + setLevelIndex(levelIndex + 1) + } else { + setVisible(false) + setValue(nextValue) + } + } + + const handleHotItemClick = (hotItem: any) => { + // 通过修改 innerValue 构造 level 数据 + const distData = findDataByName(options, hotItem.name) + // 热门城市主要是一级城市和二级城市,可以扩展。TODO + if (distData) { + const nextValue = [distData.pName, distData.name].filter( + (item) => item !== '' + ) + setInnerValue(nextValue) + setElevatorOptions(distData.children) + setLevelIndex(nextValue.length) + } + } + const renderTabs = () => { + if (!levels.length || !levels[0].name) return null + return ( + + {levels.map((item, index) => ( + + {item.name && ( + { + props.onTabsChange?.(index) + setTabActiveIndex(index) + setLevelIndex(index) + setElevatorOptions(item.children) + }} + > + {item.name} + + )} + {levels[index + 1]?.name && ( + - + )} + + ))} + + ) + } + + const renderHotCity = () => { + if (levels.length && tabActiveIndex !== 0) return null + return ( + <> + {hotCity} + MAX_LENGTH ? 'hotlist-more' : ''}`} + > + {hotList.map((item, index) => ( + handleHotItemClick(item)} + > + {item.name} + + ))} + + + ) + } + + const renderArea = () => ( + <> + {addressTip} + + handleElevatorItemClick(item, levelIndex) + } + // height="67%" + style={{ height: '100%' }} + height="93%" + /> + + ) + + const renderContent = () => ( + <> + {renderTabs()} + + {renderHotCity()} + {renderArea()} + + + ) + + return popup ? ( + setVisible(false)} + onCloseIconClick={() => setVisible(false)} + > + {renderContent()} + + ) : ( + renderContent() + ) +} + +ElevatorRender.displayName = 'NutElevatorRender' diff --git a/src/packages/address/elevatorRender.tsx b/src/packages/address/elevatorRender.tsx new file mode 100644 index 0000000000..59ffe17b42 --- /dev/null +++ b/src/packages/address/elevatorRender.tsx @@ -0,0 +1,309 @@ +import React, { FunctionComponent, useEffect, useMemo, useState } from 'react' + +import Popup from '@/packages/popup' +import Elevator from '@/packages/elevator' +import { + normalizeListOptions, + normalizeOptions, +} from '@/packages/cascader/utils' +import { transformData, findDataByName } from './utils' +import { + CascaderOption, + WebCascaderProps, + CascaderValue, + CascaderOptionKey, + RegionData, +} from '@/types' +import { ComponentDefaults } from '@/utils/typings' +import { mergeProps } from '@/utils/merge-props' +import { usePropsValue } from '@/hooks/use-props-value' +import { isEmpty } from '@/utils/is-empty' +import { useConfig } from '@/packages/configprovider' + +export interface AddressProps extends WebCascaderProps { + visible: boolean // popup visible + type: string + options: CascaderOption[] + hotList: RegionData[] + value: CascaderValue + defaultValue: CascaderValue + optionKey: CascaderOptionKey + format: Record + height: string | number +} + +const defaultProps = { + ...ComponentDefaults, + visible: false, + type: 'elevator', + options: [], + optionKey: { textKey: 'text', valueKey: 'value', childrenKey: 'children' }, + format: {}, + height: '200px', + activeColor: '', + activeIcon: 'checklist', + popup: true, + closeable: false, + closeIconPosition: 'top-right', + closeIcon: 'close', + lazy: false, + hotList: [], + onClose: () => {}, + onChange: () => {}, + onPathChange: () => {}, +} + +export const ElevatorRender: FunctionComponent< + Partial & + Omit< + React.HTMLAttributes, + 'title' | 'defaultValue' | 'onChange' + > +> = (props) => { + const { + hotList, + title, + left, + optionKey, + format, + onClose, + onChange, + onPathChange, + popup, + popupProps = {}, + visible: outerVisible, + options: outerOptions, + value: outerValue, + defaultValue: outerDefaultValue, + closeable, + closeIconPosition, + closeIcon, + } = mergeProps(defaultProps, props) + const { + locale: { + select, + address: { hotCity, selectProvince }, + }, + } = useConfig() + const classPrefix = 'nut-address' + const MAX_LENGTH = 10 + + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: [], + onChange: (value) => { + onChange(value, []) + onPathChange(value, []) + }, + }) + + const [innerOptions, setInnerOptions] = useState(outerOptions) + const [innerValue, setInnerValue] = useState(value) + const [elevatorOptions, setElevatorOptions] = useState([]) + const [addressTip, setAddressTip] = useState(selectProvince) + const [levelIndex, setLevelIndex] = useState(0) + const [tabActiveIndex, setTabActiveIndex] = useState(0) + + // 初始化数据,只格式化一次;动态数据todo + const options = useMemo(() => { + if (!isEmpty(format)) { + return transformData(normalizeListOptions(innerOptions, format)) + } + if (!isEmpty(optionKey)) { + return transformData(normalizeOptions(innerOptions, optionKey) || []) + } + return transformData(innerOptions) + }, [innerOptions, optionKey, format]) + + useEffect(() => { + setElevatorOptions(options) + }, [options]) + + const levels = useMemo(() => { + const next = [] + let end = false + let currentOptions = options + innerValue.forEach((val, index) => { + const opt = currentOptions + ?.flatMap((o: any) => o.list.find((item: any) => item.name === val)) + .filter((item) => item !== undefined)[0] + + next.push({ + name: val, + children: currentOptions, + levelIndex: index, + current: index === tabActiveIndex, + }) + if (opt?.children) { + currentOptions = opt.children + } else { + end = true + } + }) + if (!end) { + next.push({ + name: null, + children: currentOptions, + levels: -1, + current: false, + }) + } + return next + }, [innerValue, options, tabActiveIndex]) + + const [visible, setVisible] = usePropsValue({ + value: outerVisible, + defaultValue: undefined, + onChange: (value) => { + if (value === false) onClose() + }, + }) + + useEffect(() => { + if (!visible) setInnerValue(value) + }, [visible, value]) + + useEffect(() => { + setInnerOptions(outerOptions) + }, [outerOptions]) + + useEffect(() => { + setTabActiveIndex(levels.length - 1) + setAddressTip(innerValue.length ? select : selectProvince) + }, [innerValue]) + + const handleElevatorItemClick = ( + elevatorItem: RegionData, + levelIndex: number + ) => { + const nextValue = innerValue.slice(0, levelIndex) + if (elevatorItem.name) { + nextValue[levelIndex] = elevatorItem.name + } + setInnerValue(nextValue) + if (elevatorItem.children?.length) { + setElevatorOptions(elevatorItem.children) + setLevelIndex(levelIndex + 1) + } else { + setVisible(false) + setValue(nextValue) + } + } + + const handleHotItemClick = (hotItem: any) => { + // 通过修改 innerValue 构造 level 数据 + const distData = findDataByName(options, hotItem.name) + // 热门城市主要是一级城市和二级城市,可以扩展。TODO + if (distData) { + const nextValue = [distData.pName, distData.name].filter( + (item) => item !== '' + ) + setInnerValue(nextValue) + setElevatorOptions(distData.children) + setLevelIndex(nextValue.length) + } + } + const renderTabs = () => { + if (!levels.length || !levels[0].name) return null + return ( +
+ {levels.map((item, index) => ( + + {item.name && ( +
{ + props.onTabsChange?.(index) + setTabActiveIndex(index) + setLevelIndex(index) + setElevatorOptions(item.children) + }} + > + {item.name} +
+ )} + {levels[index + 1]?.name && ( +
-
+ )} +
+ ))} +
+ ) + } + + const renderHotCity = () => { + if (levels.length && tabActiveIndex !== 0) return null + return ( + <> +
{hotCity}
+
MAX_LENGTH ? 'hotlist-more' : ''}`} + > + {hotList.map((item, index) => ( +
handleHotItemClick(item)} + > + {item.name} +
+ ))} +
+ + ) + } + + const renderArea = () => ( + <> +
{addressTip}
+ + handleElevatorItemClick(item, levelIndex) + } + style={{ height: '100%' }} + height="93%" + /> + + ) + + const renderContent = () => ( + <> + {renderTabs()} +
+ {renderHotCity()} + {renderArea()} +
+ + ) + + return popup ? ( + setVisible(false)} + onCloseIconClick={() => setVisible(false)} + > + {renderContent()} + + ) : ( + renderContent() + ) +} + +ElevatorRender.displayName = 'NutElevatorRender' diff --git a/src/packages/address/existRender.taro.tsx b/src/packages/address/existRender.taro.tsx index 8440899125..96cad08658 100644 --- a/src/packages/address/existRender.taro.tsx +++ b/src/packages/address/existRender.taro.tsx @@ -15,7 +15,7 @@ export interface ExistRenderProps { } const defaultProps = { - type: 'custom', + type: 'exist', existList: [], defaultIcon: null, selectIcon: null, @@ -53,7 +53,7 @@ export const ExistRender: FunctionComponent< const onClick = (e: ITouchEvent) => { e.stopPropagation() - onSwitch && onSwitch({ type: type === 'exist' ? 'custom' : 'exist' }) + onSwitch && onSwitch({ type: type === 'exist' ? 'cascader' : 'exist' }) } return ( diff --git a/src/packages/address/existRender.tsx b/src/packages/address/existRender.tsx index 4f25e0c945..cbb0af5a01 100644 --- a/src/packages/address/existRender.tsx +++ b/src/packages/address/existRender.tsx @@ -14,7 +14,7 @@ export interface ExistRenderProps { } const defaultProps = { - type: 'custom', + type: 'exist', existList: [], defaultIcon: null, selectIcon: null, @@ -52,7 +52,7 @@ export const ExistRender: FunctionComponent< const onClick: MouseEventHandler = (e) => { e.stopPropagation() - onSwitch && onSwitch({ type: type === 'exist' ? 'custom' : 'exist' }) + onSwitch && onSwitch({ type: type === 'exist' ? 'cascader' : 'exist' }) } return ( diff --git a/src/packages/address/utils.ts b/src/packages/address/utils.ts new file mode 100644 index 0000000000..a890a4a7e1 --- /dev/null +++ b/src/packages/address/utils.ts @@ -0,0 +1,62 @@ +const generateId = (name: string) => { + return name.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) // 使用字符的 Unicode 值作为 ID +} + +export const transformData = (data: any) => { + const groupByWordCode = ( + data: any, + parentId: string | number, + parentName: string + ) => { + return data?.reduce((acc: any, item: any) => { + const { wordCode } = item + if (!acc[wordCode]) { + acc[wordCode] = [] + } + const currentId = generateId(item.value) + acc[wordCode].push({ + name: item.value, + wordCode: item.wordCode, + id: currentId, + pId: parentId, + pName: parentName, + children: + (item.children && + groupByWordCode(item.children, currentId, item.text)) || + null, + }) + return acc + }, {}) + } + + const extractTitles = (obj: any) => { + return Object.keys(obj).map((key) => ({ + title: key, + list: obj[key].map((item: any) => { + if (item.children) { + item.children = extractTitles(item.children) + } + return item + }), + })) + } + + const middleData = groupByWordCode(data, '', '') + const resultData = extractTitles(middleData) + return resultData +} + +export const findDataByName: any = (data: any, name: string) => { + for (const item of data) { + if (item.name?.indexOf(name) === 0) return item + if (item.children) { + const found = findDataByName(item.children, name) + if (found) return found + } + if (item.list) { + const found = findDataByName(item.list, name) + if (found) return found + } + } + return null // 如果没有找到,返回 null +} diff --git a/src/packages/cascader/cascader.taro.tsx b/src/packages/cascader/cascader.taro.tsx index fec6ebcef9..5267433cc1 100644 --- a/src/packages/cascader/cascader.taro.tsx +++ b/src/packages/cascader/cascader.taro.tsx @@ -22,7 +22,7 @@ import { mergeProps } from '@/utils/merge-props' import { usePropsValue } from '@/hooks/use-props-value' import { isEmpty } from '@/utils/is-empty' import { getRefValue, useRefState } from '@/hooks/use-ref-state' -import { useConfig } from '@/packages/configprovider' +import { useConfig } from '@/packages/configprovider/index.taro' const defaultProps: TaroCascaderProps = { ...ComponentDefaults, diff --git a/src/packages/elevator/elevator.tsx b/src/packages/elevator/elevator.tsx index 0c30b54d05..6d84e20a85 100644 --- a/src/packages/elevator/elevator.tsx +++ b/src/packages/elevator/elevator.tsx @@ -274,7 +274,7 @@ export const Elevator: FunctionComponent< className={classNames({ [`${classPrefix}-bars-inner-item`]: true, [`${classPrefix}-bars-inner-item-active`]: - item[floorKey] === list[currentIndex][floorKey], + item[floorKey] === list[currentIndex]?.[floorKey], })} data-index={index} key={index} diff --git a/src/sites/sites-react/doc/docs/react/migrate-from-v2.md b/src/sites/sites-react/doc/docs/react/migrate-from-v2.md index cf2fd267e5..21693e359d 100644 --- a/src/sites/sites-react/doc/docs/react/migrate-from-v2.md +++ b/src/sites/sites-react/doc/docs/react/migrate-from-v2.md @@ -149,6 +149,11 @@ plugins: [ ### 数据录入 +#### Address + +- 修订原 type 的类型定义范围,改 `custom` 为 `cascader`,语义化更强;增加 `elevator`,便于快速检索地址。如果没有用到过 `exsit` 与 `custom` 切换逻辑的,理论上无需关注该变化。 +- 电梯模式,支持热门城市、级联地址按照电梯的方式展示。 + [//]: # '#### Calendar' #### Cascader @@ -314,7 +319,6 @@ plugins: [ [//]: # '#### VirtualList' [//]: # '#### WaterMark' [//]: # '### 特色组件' - [//]: # '#### Address' [//]: # '#### Barrage' [//]: # '#### Card' [//]: # '#### Signature' diff --git a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md index 8e5f97ab0c..6da127ec1b 100644 --- a/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md +++ b/src/sites/sites-react/doc/docs/taro/migrate-from-v2.md @@ -149,6 +149,11 @@ plugins: [ ### 数据录入 +#### Address + +- 修订原 type 的类型定义范围,改 `custom` 为 `cascader`,语义化更强;增加 `elevator`,便于快速检索地址。如果没有用到过 `exsit` 与 `custom` 切换逻辑的,理论上无需关注该变化。 +- 电梯模式,支持热门城市、级联地址按照电梯的方式展示。 + [//]: # '#### Calendar' #### Cascader diff --git a/src/types/spec/address/base.ts b/src/types/spec/address/base.ts index e4ef3a5f5f..4538f688c0 100644 --- a/src/types/spec/address/base.ts +++ b/src/types/spec/address/base.ts @@ -3,16 +3,9 @@ import { CascaderOption, CascaderOptionKey, CascaderValue } from '@/types' export interface RegionData { name?: string - [key: string]: any } -export interface ChangeData { - next: string - value: string | RegionData - custom: string -} - export interface CloseCallBackData extends Regions { addressIdStr: string addressStr: string @@ -40,13 +33,6 @@ export interface AddressList { phone?: string } -export interface CurrentData { - next: string - value: string | RegionData - custom: string - selectedRegion?: Regions -} - export interface Regions { province: RegionData city: RegionData @@ -54,12 +40,14 @@ export interface Regions { town: RegionData } +export type AddressModeType = 'exist' | 'cascader' | 'elevator' + export interface BaseAddress { visible: boolean defaultVisible: boolean value?: CascaderValue defaultValue?: CascaderValue - type: string + type: AddressModeType options: CascaderOption[] optionKey: CascaderOptionKey format: Record @@ -69,6 +57,7 @@ export interface BaseAddress { defaultIcon: React.ReactNode selectIcon: React.ReactNode backIcon: React.ReactNode + hotList: RegionData[] onSwitch?: (data: { type: string }) => void onExistSelect?: (data: AddressList) => void } diff --git a/src/types/spec/cascader/base.ts b/src/types/spec/cascader/base.ts index dd65e66eab..873e78ce2f 100644 --- a/src/types/spec/cascader/base.ts +++ b/src/types/spec/cascader/base.ts @@ -10,6 +10,7 @@ export interface CascaderPane { export interface CascaderOption { text?: string value?: number | string + wordCode?: string paneKey?: string disabled?: boolean children?: CascaderOption[]