效果图
需求分析
级联菜单分为两部分:head与body。
body
包含两部分:已选项列表,候选菜单
已选项列表
- body展示当前菜单的所有option,可上下滚动。
- body中选一个option后,会在head的已选列表中进行展示,并且body将显示下一级的菜单。
- 选中的option,背景色和字体需要改变,以示区分。
候选菜单
- 依次显示每个选中的option,当所有option的长度超过屏幕宽度时,可左右滚动
- 每个option固定宽度,超出宽度显示省略号。
- 当点击其中一个option时候,该项高亮,并且body显示为该级的菜单。
head
- 包含取消和确定两个按钮。
- 点击取消,将不做任何处理
- 确定按钮需要在级联菜单选到没有下一级的时候才可点击
- 点击确定,将触发回调,携带已选的参数
项目结构
├─ src
│ ├─ App.js
│ ├─ components
│ │ └─ Cascader
│ │ ├─ CascaderContent // content部分
│ │ │ ├─ CascaderContent.js
│ │ │ └─ style.js
│ │ ├─ CascaderHead // head部分
│ │ │ ├─ CascaderHead.js
│ │ │ └─ style.js
│ │ ├─ index.js // 入口
│ │ ├─ style.js
│ │ ├─ Cascader.js
│ │ └─ testData // 测试数据
│ │ └─ data.js
│ ├─ index.css
│ └─ index.js
实现body部分
levels
根据数据源dataSource与value生成一个数组,数据结构如下。
数组的长度为已选项数加一,最大为数据源的最大深度
onChange
点击菜单项,如果为不可选状态,则return。如果有onSelect回调,则将已选的value传递给回调函数
value
初始化的时候,value为默认值,后面在此基础上进行修改
loading
有时候数据可能是异步请求获取的,增加一个定时器,可以在数据未加载完的时候,显示loading效果。
tabActiveIndex
当前候选的菜单的索引,,选中一项后,值加一,如果已经选到了最大深度,那么索引为最后一页。
classPrefix
是一个变量,方便设置公共变量
import React, { useMemo, useState } from "react";
import { useCallback, useEffect } from "react";
import { Wrapper } from "./style";
const classPrefix = `antdm-cascader-view`
export const CascaderContent = function ({ visible = false, ...props }) {
// 当前页
const [tabActiveIndex, setTabActiveIndex] = useState(0);
// 初始值
const [value, setValue] = useState(props.value || props.defaultValue || []);
// loading效果
const [loading, setLoading] = useState(true);
const levels = useMemo(() => {
const ret = []
let currentOptions = props.options
let reachedEnd = false
for (const v of value) {
const target = currentOptions.find(option => option.value === v)
ret.push({
selected: target,
options: currentOptions,
})
// 没有下一项的时候中止遍历
if (!target || !Array.isArray(target.children) || target.children.length === 0) {
reachedEnd = true
break
}
currentOptions = target.children
}
if (!reachedEnd) {
ret.push({
selected: undefined,
options: currentOptions,
})
}
return ret;
}, [props.options, value])
// 点击选项的时候
const onChange = useCallback((item, index) => {
if (item?.disabled) {
return
}
const newValue = [...value.slice(0, index), item.value];
setValue(newValue);
props.onSelect?.(newValue)
}, [value, props.onSelect])
// 选中数据后,切换下一级菜单
useEffect(() => {
const max = levels.length - 1
if (tabActiveIndex > max) {
setTabActiveIndex(max)
}
}, [tabActiveIndex, levels])
useEffect(() => {
setTabActiveIndex(levels.length - 1)
}, [value])
useEffect(() => {
if (visible) {
setValue(props.value || props.defaultValue || []);
}
}, [visible])
useEffect(() => {
setValue(props.value || props.defaultValue || [])
}, [props.value, props.defaultValue])
// 设置定时器,使用loading效果
useEffect(() => {
const timer = setTimeout(() => {
if (props.options?.length === 0) {
setLoading(false)
}
return () => {
clearTimeout(timer)
}
}, 3000);
}, [])
// 数据加载完毕后取消loading效果
useEffect(() => {
if (props.options.length !== 0) {
setLoading(false)
}
}, [props.options])
return <Wrapper>
<div className={classPrefix}>
<div className={`${classPrefix}-tabs`}>
{levels.map((item, index) => {
return <div
key={index}
onClick={() => { setTabActiveIndex(index) }}
className={`${classPrefix}-tab ${tabActiveIndex === index && classPrefix + "-tab-active"}`}>
{item?.selected?.label ? item?.selected?.label : item?.selected?.label === "" ? "" : "请选择"}
</div>
})}
</div>
<div className={`${classPrefix}-content`}>
{!loading ? levels.map((item, index) => {
return <div
key={index.toString()}
style={{ display: index === tabActiveIndex ? "block" : "none" }}
className={`${classPrefix}-list`} >
{item.options.map((o, i) => {
return <div key={i.toString()} className={`${classPrefix}-item ${o.value === item?.selected?.value && classPrefix + "-item-active"}`}>
<div
onClick={() => onChange(o, index)}
className={`${classPrefix}-item-main ${o?.disabled && classPrefix + "-item-disabled"}`}>
{o.label}
</div>
{o.value === item?.selected?.value && <div className={`${classPrefix}-item-extra`}>✓</div>}
</div>
})}
</div>
}) : "loading..."}
</div>
</div>
</Wrapper>
}
实现head部分
当已经没有下一级菜单的时候,确定按钮变为可点击状态
import React from "react";
import { Wrapper } from "./style";
const classPrefix = `antdm-cascader`
export const CascaderHead = function (props) {
return <Wrapper>
<div className={classPrefix}>
<div className={`${classPrefix}-header`}>
<a
className={`${classPrefix}-header-button`}
onClick={() => {
props.onCancel?.()
}}
>
{props.cancelText || "取消"}
</a>
<div className={`${classPrefix}-header-title`}>{props.title}</div>
<a
className={`${classPrefix}-header-button ${props.canCommit ? '' : classPrefix + '-header-confirm-disable'}`}
onClick={() => {
props.canCommit && props.onConfirm?.();
}}
>
{props.confirmText || "确定"}
</a>
</div>
</div>
</Wrapper>
}
整合head与body
import React, { useState, useCallback, useEffect } from "react";
import { Popup } from "antd-mobile";
import { CascaderHead } from "./CascaderHead/CascaderHead";
import { CascaderContent } from "./CascaderContent/CascaderContent";
import { Wrapper } from "./style";
export const CascaderModal = function (props) {
const [value, setValue] = useState(props.value || props.defaultValue || []);
const [canCommit, setCanCommit] = useState(false);
const onChange = useCallback((v) => {
setValue(v);
props.onSelect?.(v)
}, [props.onSelect])
// 将选择的数据提交出去
const onConfirm = useCallback(() => {
props.onConfirm?.(value)
}, [props.onConfirm, value])
// 取消
const onCancel = useCallback(() => {
props.onCancel?.()
}, [props.onCancel])
useEffect(() => {
if (value.length === 0) {
return;
}
let children = props.options;
let i = 0;
for (i; i < value.length; i++) {
const obj = children.find(item => item.value === value[i]);
if (!obj) {
children = undefined;
break;
} else {
children = obj.children
}
}
setCanCommit(!Array.isArray(children) || children.length === 0)
}, [value, props.options])
useEffect(() => {
setValue(props.value || props.defaultValue || [])
}, [props.value, props.defaultValue])
useEffect(() => {
if (props.visible) {
setCanCommit(false);
}
}, [props.visible])
return <Wrapper>
<Popup
className="antdm-cascader-modal"
visible={props.visible}
onClose={onCancel}
animationType="slide-up"
popup={true}
>
<CascaderHead {...props} canCommit={canCommit} onCancel={onCancel} onConfirm={onConfirm} />
<CascaderContent {...props} visible={props.visible} onSelect={onChange} />
</Popup>
</Wrapper>
}
以上就是React实现antdM的级联菜单实例的详细内容,更多关于React antdM级联菜单的资料请关注编程网其它相关文章!