react层级联动,城市级联选择,多级联动组件

基于antd实现自定义多级层级组件,主要使用react+antd+typescript实现具体内容:

组件展示效果:
在这里插入图片描述数据json格式:

[{
	text: '浙江省',
	value: '330000',
	children: [{
		text: '杭州市',
		value: '330100',
		children: [{
			text: '上城区',
			value: '330102',
		}, {
			text: '下城区',
			value: '330103',
		}, {
			text: '江干区',
			value: '330104',
		}]
	}, {
		text: '宁波市',
		value: '330200',
		children: [{
			text: '海曙区',
			value: '330203',
		}, {
			text: '江北区',
			value: '330205',
		}, {
			text: '北仑区',
			value: '330206',
		}]
	}, {
		text: '温州市',
		value: '330300',
		children: [{
			text: '鹿城区',
			value: '330302',
		}, {
			text: '龙湾区',
			value: '330303',
		}, {
			text: '瓯海区',
			value: '330304',
		}]
	}]
}, {
	text: '江苏省',
	value: '320000',
	children: [{
		text: '南京市',
		value: '320100',
		children: [{
			text: '玄武区',
			value: '320102',
		}, {
			text: '秦淮区',
			value: '320104',
		}, {
			text: '建邺区',
			value: '320105',
		}]
	}, {
		text: '无锡市',
		value: '320200',
		children: [{
			text: '锡山区',
			value: '320205',
		}, {
			text: '惠山区',
			value: '320206',
		}, {
			text: '滨湖区',
			value: '320211',
		}]
	}, {
		text: '徐州市',
		value: '320300',
		children: [{
			text: '鼓楼区',
			value: '320302',
		}, {
			text: '云龙区',
			value: '320303',
		}, {
			text: '贾汪区',
			value: '320305',
		}]
	}]
}]

CascaderList.tsx 组件源码:

import { useState, memo, useMemo, useEffect } from 'react';
import { Popover, Cascader } from 'antd';
import { CheckOutlined, CaretDownOutlined, CloseCircleFilled } from '@ant-design/icons';
import '@/assets/style/cascaderList.less';

type objTypes = {
	[key: string]: any
}

type fileNameTypes = {
	text: string,
	value: string,
	children: string
}

type propTypes = {
	column?: number, //几级层级联动
	fileNames?: fileNameTypes, //选项列表中的属性{text: 'text', value: 'value', children: 'children'}
	options: Array<objTypes>, // 配置选项的列表
	checkedValue?: Array<any>, //选中的数据
	placeholder?: string, //输入占位符
	separator?: string, //分隔符
	onFinish?: Function, //全部选中的回调函数
	render?: Function, //自定义渲染内容
	children?: JSX.Element
}

// 将树形结构转换成数组
function treeToList(tree: Array<objTypes>) {
	let queue = [...tree];
	let res: Array<objTypes> = [];
	queue.concat(tree);
	while (queue.length != 0) {
		let obj = queue.shift();// 弹出队首元素
		if (obj?.children) {
			queue = queue.concat(obj.children);// 子节点入队
			// delete obj["children"];// 删除 children 属性
		}
		res.push(obj as objTypes);
	}
	return res;
};

// 配置显示的导航
function configNav<T>(checkedList: Array<T>, col: number): Array<T> {
	let arr: Array<T> = [];
	if (checkedList.length >= col) {
		arr = checkedList;
	} else {
		arr = [...checkedList, '请选择' as T];
	}
	return arr;
};

// 将字段转换显示
function renderTextCallback<T>(checkedValue: Array<T>, allList: Array<objTypes>, fileNames: fileNameTypes, keyText: string): Array<T> {
	if (!checkedValue.length) return [];
	let key = keyText === 'text' ? fileNames.value : fileNames.text;
	return checkedValue.map(item => {
		let findItem = allList.find((el: objTypes) => el[key] === item);
		return (findItem ? findItem[keyText] : '');
	})
};

/* 渲染层级联动列表 */
function CascaderList(props: propTypes) {

	const { column = 3, fileNames = {
		text: 'text',
		value: 'value',
		children: 'children'
	},
		checkedValue = [], options = [], separator = '/', placeholder, onFinish, render } = props;

	const allCascaderList = useMemo(() => {
		return treeToList(options);
	}, [])

	// 控制弹框显示
	const [open, setOpen] = useState(false);
	// 当前选中数据
	const [checkedList, setCheckedList] = useState<string[]>(() => renderTextCallback<string>(checkedValue, allCascaderList, fileNames, 'text'));
	// 当前渲染选中
	const [renderCheckedText, setRenderCheckedText] = useState(checkedList);
	// 当前选中的导航
	const [activeKey, setActiveKey] = useState(checkedValue.length ? checkedValue.length - 1 : 0);
	// 配置导航
	const [tabList, setTabList] = useState(() => configNav(checkedList, column));

	useEffect(() => {
		if (!checkedValue.length) {
			setCheckedList([]);
			setRenderCheckedText([]);
			setActiveKey(0);
			setTabList(configNav([], column));
		}
	}, [JSON.stringify(checkedValue)])


	/* 修改导航变化 */
	const onChange = (key: number) => {
		if (key === activeKey) return;
		setActiveKey(key);
		let newTabList = tabList.filter(item => item !== '请选择');
		if (JSON.stringify(newTabList) !== JSON.stringify(tabList)) {
			setTabList(newTabList);
		};
	};
	/* 点击选中层级 */
	const handleSelectCascader = (e: any, label: string) => {
		e.stopPropagation();
		// 设置选中数据
		let newCheckedList = checkedList.slice(0, activeKey);
		newCheckedList.push(label);
		setCheckedList(newCheckedList);
		if (newCheckedList.length === column) {
			// 如果选中的数据与层级联动数相同就完成选择
			onSelectCascaderFinish(newCheckedList)
		} else {
			// 设置导航
			let newTabList = configNav(newCheckedList, column);
			setTabList(newTabList);
			setActiveKey(activeKey + 1);
		}
	};
	/* 配置渲染的列表 */
	const renderCallback = (checkedList: string[], tabKey: number): objTypes => {
		if (!checkedList.length || tabKey === 0) return options;
		return allCascaderList.find(item => item.text === checkedList[tabKey - 1])?.children;
	};
	/* 当前选中信息 */
	const checkSelectOption = (text: string, key: number) => {
		return checkedList[key] === text;
	};
	/* 完成全部选择 */
	const onSelectCascaderFinish = (selectValues: Array<string>) => {
		setOpen(false);
		setRenderCheckedText(selectValues);
		// 将数据转换成value形式导出使用
		const values = renderTextCallback(selectValues, allCascaderList, fileNames, 'value');
		onFinish && onFinish(values);
	};
	/* 点击清除信息 */
	const handleClear = (e: any) => {
		e.stopPropagation();
		onSelectCascaderFinish([]);
		setCheckedList([]);
	};
	/* 关闭选择框 */
	const handleOpenChange = (newOpen: boolean) => {
		setOpen(newOpen);
		if (newOpen) {
			let newTabList = configNav(checkedList, column);
			setTabList(newTabList);
			setActiveKey(checkedList.length ? checkedList.length - 1 : 0);
		}
	};

	// 渲染三级选择列表
	const CascaderContext = () => (
		<>
			{/* ------- 层级联动导航列表 ------- */}
			<div className='cascader-tab-select cursor' onClick={(e) => e.stopPropagation()}>
				{tabList.map((item, idx) => (
					<div
						onClick={() => onChange(idx)}
						className={`cascader-tab-item ${activeKey === idx ? 'active' : ''} hover-color`}
						key={'cascader-tab-item-' + idx}>{item}</div>
				))}
			</div>
			{/* ----------- 层级联动选项展示 ------------- */}
			<ul className='cascader-list-wrapper'>
				{renderCallback(checkedList, activeKey)?.map((item: objTypes) => (
					<li key={item.value} onClick={(e) => handleSelectCascader(e, item.text)}
						className={`${checkSelectOption(item.text, activeKey) ? 'checked-cascader' : ''} cursor`}
					>
						{checkSelectOption(item.text, activeKey) ? <CheckOutlined className='checked-cascader-icon primary-color' /> : null}
						<span>{item.text}</span>
					</li>
				))}
			</ul>
		</>
	)

	return (
		<>
			<Popover overlayClassName='cascader-popover-wrapper' content={<CascaderContext />}
				trigger="click" placement='bottom' open={open} arrow={false}
				getPopupContainer={(triggerNode: any) => triggerNode?.parentNode}
				onOpenChange={handleOpenChange}>
				{render ? render(renderCheckedText) :
					<Cascader allowClear open={false} value={renderCheckedText} options={undefined}
						placeholder={placeholder || '请选择'} displayRender={() => checkedList.join(separator)}
						suffixIcon={<CaretDownOutlined />} clearIcon={<CloseCircleFilled onClick={handleClear} />}
						className='cascader-render-input' style={{ width: '100%' }} />
				}
			</Popover>
		</>
	)
}

export default memo(CascaderList);

less样式:

.cascader-popover-wrapper {
    width: 100%;

    .ant-popover-inner {
        padding: 0;
        border-radius: 2px;
        border: 1px solid #e2e6ed;
        max-height: 300px;
        font-size: 12px;
        box-shadow: 0 4px 12px 0 rgba(56, 56, 56, .15);
    }
}

.cascader-tab-select {
    display: flex;
    align-items: center;
    height: 40px;
    line-height: 40px;
    padding-left: 36px;
    color: rgba(0, 0, 0, 0.65);
    border-bottom: 1px solid #f0f0f0;

    .cascader-tab-item {
        margin-right: 24px;

        &:last-of-type {
            margin-right: 0;
        }
    }
}

.cascader-list-wrapper {
    max-height: 260px;
    padding: 0 4px;
    box-sizing: border-box;
    overflow: auto;

    li {
        position: relative;
        height: 30px;
        line-height: 30px;
        padding-left: 32px;
        color: #3d4757;
        margin: 4px 0;

        &:not(.checked-cascader):hover {
            background-color: #f2f3f5;
        }

        .checked-cascader-icon {
            position: absolute;
            left: 10px;
            top: 10px;
        }
    }

    .checked-cascader {
        color: @primary-color;
        background-color: #f2f3f5;
    }
}

.cascader-render-input {
    border-radius: 2px;

    &:focus {
        box-shadow: none;
    }
}

使用方法:

//1. 引入组件
import CascaderList from '@/components/CascaderList';

//2. 在页面使用
<CascaderList column={3} options={options} checkedValue={value} placeholder='请选择' 
onFinish={onFinish} separator=/’ fileNames={fileNames} render={render}  />

// column 代表层级数,默认是 3
// options 代表层级联动的数据,比如上面的json数据
// checkedValue 代表选中的数据数组,是上面json数据中value的值
// separator 代表分隔符,默认是 /
// fileNames 代表json数据中的数据配置
/*
fileNames = {
	text: 'text',
	value: 'value',
	children: 'children'
}
*/
// render 是一个回调函数,参数是当前选中的text内容,可用于自定义页面显示内容
// onFinish 是选择完成的函数,参数是当前选中的value内容

最后你把代码复制到项目中就可以使用了,相比于传统的层级联动,页面显示效果更好!开发不易,希望得到老铁们的支持,收藏点赞加分享这是对于我们分享源码的动力!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值