仿京东地址选择组件
本篇文章记录我在项目期间做的一个地址选择组件的优化。
一、先看下原始功能,上图
三级地址数据全部一次性请求出来,接口响应时间共20.18s,真是惊掉下巴,这给用户使用肯定会被吐槽吧。
为了避免被吐槽,赶紧优化起来。
二、代码说明
1.组件
<AddressPicker
title="请选择所在地区"
visible={this.state.addressPickerVisible}
areaInfo={this.state.addressPickerValue}
areaIds={this.state.addressPickerValueData}
onRef={this.onRef}
handleValue={this.handleValue}
closeModal={(e) => this.hideAddressPickerModal(e)}
/>
组件名称:AddressPicker
参数:
@param1: title,组件的标题
@param2: visible,弹框的显示与隐藏 boolean
@param3: areaInfo,地址数据详情,回显的时候用
@param4: areaIds,地址数据id
@param5: onRef,固定写法,父组件需要调用子组件的方法
@param6: handleValue,最终选择的地址赋值,展示
@param7: closeModal,弹框关闭事件
功能描述:此组件是一个弹出框,visible控制弹框显示隐藏,areaInfo和areaId是组件所选中的地址详情和id,handleValue方法用来给当前页面设置地址值,将areaInfo和areaId的值存下来供form提交。
三个方法贴一下:
hideAddressPickerModal = (e) => {
if(e && (e.target.className == 'add-picker-modal show-add-picker-modal' || e.target.nodeName == 'LI')){
this.setState({
addressPickerVisible: false
},()=>{
this.child.closeModalCallback();
})
}
}
onRef = (ref) => {
this.child = ref
}
handleValue = (value, navValue) => {
this.setState({
addressPickerValue: value,
addressPickerValueData: navValue
});
}
2.容器
input是从弹出框里选择完地址后需要保存的容器。
<p className="input-box-default">
<span>所在地区:</span>
<input type="text" placeholder="省/市/区"
readOnly
value={this.sate.addressPickerValue}
data-value={this.state.addressPickerValueData}
onClick={this.showAddressPickerModal}
/>
</p>
参数:
@param1:value,展示的内容,如:北京,朝阳区,三环内
@param2:data-value,areaId,这个参数是实际三级地址id列表,如:[1,12,1212]
@param3:onClick,点击此input触发地址选择弹框的展示
功能描述:点击此input容器,弹出地址选择弹框,进行地址的选择,选择完成后,将选择的地址回传到此容器内,展示给用户。
3.组件代码详细
import React, { Component, Children } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react';
import './addressPicker.less';
import 'swiper/swiper.min.css';
import * as api from './addressApi';
// install Swiper modules
class AddressPicker extends Component {
constructor(props) {
super(props);
this.state = {
visible: props.visible,
title: props.title,
areaIds: props.areaIds,
areaPanel: [{ areaId: '0', areaName: '请选择', areaDeep: 0 }],
areaDataDeep: [], //把三级数据存起来,以便在切换tag的时候不再请求接口
navActive: 0,
isChange: false,
newArr: [], //数据回显示
}
this.swiper = null;
}
componentWillReceiveProps(nextProps) {
this.setState({
visible: nextProps.visible,
title: nextProps.title,
areaIds: nextProps.areaIds
})
//地址回显
if (nextProps.visible) {
let { areaIds, areaInfo } = nextProps;
areaInfo = areaInfo.split(',');
if (areaIds) { //表示编辑
let newArr = [];
for (let i = 0; i < areaIds.length; i++) {
newArr.push({
areaId: areaIds[i],
areaName: areaInfo[i],
areaDeep: i + 1
})
}
this.setState({
newArr,
areaPanel: newArr //回显的时候直接设置这个值,避免请求接口慢显示不流畅
})
// newArr.map(async (item, index) => {
// await this.selectAdd(item[0])
// })
//数据回显的时候调接口,需要同步调。等第一次返回再调第二次,undefined是e的点位符
this.selectAdd(newArr[0],undefined,0)
}
}
}
componentDidMount() {
this.props.onRef(this)
this.swiper = document.querySelector('.swiper-container').swiper;
this.getAreaList();
}
getAreaList = (params = {}, e,index) => {
let { areaParentId, areaName, areaDeep = 1 } = params || {};
let { areaPanel, newArr } = this.state;
if(areaDeep == 3){
this.setState({isChange: true})
}
if (areaParentId && index == undefined) { //回显的时候不走这个判断
if (areaDeep == 3) { //三级就不做操作了,把最后一个"请选择"标签删除
areaPanel.splice(areaPanel.length - 1, 1, { areaId: areaParentId, areaName, areaDeep });
this.setState({
areaPanel
})
return false;
}
this.setTabData(params)
}
if(index == 2) return false; //跳出递归调用 selectAdd方法
api.getAreaDataList({ parentAreaId: areaParentId }).then(res => {
if (res.result == '1') {
this.setListData(params, res.data);
//这一层级没有数据,删除最后一个“请选择”
if (res.data.length == 0 && index == undefined) {//回显的时候不走这个判断
areaPanel.splice(areaPanel.length - 1, 1);
this.setState({
areaPanel,
})
this.setState({
navActive: 2
})
e && this.props.closeModal(e);
}
setTimeout(() => {
this.setState({ isChange: true },()=>{
//如果存在index,表示数据回显,需要递归调用selectAdd方法。
if(index != undefined){
this.selectAdd(newArr[index+1],e,index+1)
}
})
}, 300);
}
})
}
// body内容数据的变更处理
setListData = (params, data) => {
let { areaDeep = 0 } = params;
let { areaDataDeep } = this.state;
let isExit = false;
let iIndex = 0;
for (let index in areaDataDeep) {
if (areaDataDeep[index].areaDeep == areaDeep) {
isExit = true;
iIndex = index;
break;
}
}
if (isExit) { //如果存在
areaDataDeep.splice(iIndex, 1, { areaDeep, data }); //把二级数据替换
// if(areaDeep == 1){
// areaDataDeep.splice(areaDataDeep.length-1, 1); //把三级数据清除
// }
} else {
areaDataDeep.push({ areaDeep, data });
}
//这一层级没有数据,那么删除最后一个数据
if (data.length == 0 && areaDataDeep.length == 3) {
areaDataDeep.splice(areaDataDeep.length - 1, 1)
}
this.setState({
areaDataDeep
}, () => {
//这一层级没有数据,那么不用切换了
if (data.length != 0) {
this.swiper.slideTo(this.swiper.activeIndex + 1, 300, false)
}
})
}
// 顶部tab数据处理
setTabData = (params) => {
const { areaPanel, areaIds } = this.state;
let { areaParentId, areaName, areaDeep = 1 } = params;
//以下代码处理选中哪个级并修改相应的值
let isExit = false;
let iIndex = 0;
for (let index in areaPanel) {
if (areaPanel[index].areaDeep == areaDeep) {
isExit = true;
iIndex = index;
break;
}
}
if (isExit) {
areaPanel.splice(iIndex, 1, { areaId: areaParentId, areaName, areaDeep });
} else {
areaPanel.splice(areaPanel.length - 1, 0, { areaId: areaParentId, areaName, areaDeep });
}
//以下代码处理修改完值后,把后面的变成“请选择”
if (areaDeep == 1) {
areaPanel.splice(1, 2, { areaId: '0', areaName: '请选择', areaDeep: 0 });
}
if (areaDeep == 2) {
areaPanel.splice(2, 1, { areaId: '0', areaName: '请选择', areaDeep: 0 });
}
this.setState({ areaPanel })
}
//modal关闭后的回调
closeModalCallback = () => {
const { areaPanel } = this.state;
let isHandle = false;
let navHtml = areaPanel.map(item => {
if (item.areaId == 0) { //表示没有选择完地址
isHandle = true;
}
if (item.areaName != '请选择') {
return item.areaName
} else {
return ''
}
})
let navValue = areaPanel.map(item => {
if (item.areaId != 0) {
return item.areaId
} else {
return ''
}
})
navHtml.forEach((item, index) => {
if (item.length == 0) {
navHtml.splice(index, 1)
}
})
navValue.forEach((item, index) => {
if (item.length == 0) {
navValue.splice(index, 1)
}
})
if (!isHandle) this.props.handleValue(navHtml.join(','), navValue, event)
}
//设置滚动条
setScrollHeight = () => {
//切换的时候找到active,设置一下滚动条,必须放在这个位置写
let swiperSlide = document.querySelectorAll(".swiper-slide");
let ul = swiperSlide[this.swiper.activeIndex].querySelector('.slide-inner-cont')
let activeLi = ul.querySelector('.active');
ul.scrollTop = activeLi && activeLi.offsetTop
}
slideChange = () => {
console.log('change');
this.setState({
navActive: this.swiper.activeIndex
},()=>{
this.setScrollHeight();
})
}
changeNavTag = (index, e) => {
e.stopPropagation();
this.swiper.slideTo(index, 300, false)
}
//选择地址
// this.selectAdd(newArr[0],e,index),index是数据回显时候使用
selectAdd = (data, e, index) => {
const { isChange, areaPanel } = this.state;
e && e.stopPropagation();
if (!isChange) return;
if(data.areaDeep == 3 && areaPanel.length == 2) return; //当点击的是最后一级并且顶部tab是两个数据,那么表示没有选第二级直接选了第三级,直接退出,不进行下列操作。
if(index == undefined){ //回显的时候不走这个判断
this.setState({
navActive: data.areaDeep
})
if (data.areaDeep == 3) { //如果是第三级,或没有子级,那么关闭
this.setState({
navActive: 2
})
e && this.props.closeModal(e);
}
}
let params = {
areaParentId: data.areaId,
areaName: data.areaName,
areaDeep: data.areaDeep
}
this.setState({ isChange: false }, () => {
this.getAreaList(params, e,index)
})
}
render() {
const { visible, title, areaDataDeep, areaPanel, navActive } = this.state;
const { closeModal } = this.props;
if (!areaDataDeep) return false;
if (!areaPanel) return false;
let liActive = '';
return <div
onClick={closeModal}
className={visible ? 'add-picker-modal show-add-picker-modal' : 'add-picker-modal'}>
<div className="add-picker-panel">
<div className="title">{title}</div>
<div style={{ height: '89%' }}>
<div className="nav-title">
<ul>
{
areaPanel.map((item, index) => {
if (index == navActive) {
liActive = item.areaId;
}
return <li key={index} className={`${index == navActive ? 'active' : ''}`} onClick={(e) => { this.changeNavTag(index, e) }}>{item.areaName}</li>
})
}
</ul>
</div>
<div className="pick-swiper">
<Swiper
onSwiper={(swiper) => console.log(swiper)}
onSlideChange={() => this.slideChange()}
>
{
areaDataDeep.map((item, index) => {
return <SwiperSlide key={index}>
<div className="slide-inner-cont">
<ul>
{
item.data.map((it, ind) => {
return <li key={ind} className={`${it.areaId == liActive ? 'active' : ''}`}
onClick={(e) => this.selectAdd(it, e)}
>{it.areaName}</li>
})
}
</ul>
</div>
</SwiperSlide>
})
}
</Swiper>
</div>
</div>
</div>
</div>
}
}
export default AddressPicker;
样式addressPicker.less
.add-picker-modal{
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: rgba(0,0,0,.5);
z-index: 1991;
opacity: 0;
visibility: hidden;
transition: all .3s;
}
.show-add-picker-modal{
opacity: 1;
visibility: visible;
.add-picker-panel{
transform: translateY(0%);
}
}
.add-picker-panel{
width: 100%;
height: 80%;
background: #fff;
bottom: 0;
position: absolute;
left: 0;
border-radius: 20px 20px 0 0;
overflow-y: auto;
transition: all .3s;
transform: translateY(101%);
box-sizing: border-box;
.title{
padding: 30px 0 10px 0;
font-size: 36px;
text-align: center;
font-weight: 400;
color: #333333;
}
.nav-title{
border-bottom: 1px solid #EBEBEB;
padding: 0 35px;
ul{
display: flex;
align-items: center;
height: 100px;
}
li{
margin-right: 20px;
line-height: 100px;
max-width: 30%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.active{
color: rgb(212, 40, 45);
}
}
}
.slide-inner-cont{
width: 100%;
height: 100%;
overflow-y: auto;
li{
line-height: 1rem;
&.active{
color: rgb(212, 40, 45);
}
}
}
::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.pick-swiper{
padding: 15px 35px;
height: 91%;
box-sizing: border-box;
}
.swiper-container{
height: 100%;
}
}
/*
address.less
.wx-addresslist {
.wx-addresslist-bar {
box-sizing: border-box; // height: 1.1rem;
position: fixed;
bottom: 0;
width: 100%;
transition: bottom 0.2s;
z-index: 100;
height: 90px;
.am-button{
border-radius: 0 !important;
height: 90px !important;
line-height: 90px !important;
}
}
.fix-scroll{
padding: 20px 20px 0 20px;
box-sizing: border-box;
background: #F0F1F2;
}
}
.wx-address-add {
.am-list-item .am-input-label.am-input-label-5 {
width:170px;
font-size: 28px;
}
.am-list-item.am-input-item{
height: 100px;
overflow: auto;
overflow-x: hidden;
}
.add-address-line{
.am-list-extra{
flex-basis: 68%;
text-align: left;
&.selectTxt{
color: #bbb !important;
}
}
}
.beizhu-box{
margin: 20px 0 0 0 !important;
border-radius: 10px;
border-bottom: none !important;
}
.beizhu-textarea{
padding: 20px 0 0;
textarea{
border:none;
height: 120px;
width: 540px;
margin-left: 52px;
margin-top: 0px;
box-sizing: border-box;
vertical-align: top;
padding:0 10px;
line-height: 40px;
}
}
}
.address-list-manage{
margin-bottom: 20px;
.am-list-body{
border-radius: 5px;
.am-list-item.am-list-item-middle{
&:last-child{
border-bottom: 0;
}
}
}
.add-manage-det{
font-size: 28px;
.am-list-content{
padding: 20px 0 !important;
}
p{
margin: 0;
font-size: 28px;
span{
display: inline-block;
margin-right: 20px;
vertical-align: middle;
}
.bold{
max-width: 427px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.order-address-Info{
word-break: break-all;
white-space:normal ;
font-size: 28px;
}
.am-list-line .am-list-brief{
font-size: 28px;
}
}
.add-manage-btn{
.am-flexbox.am-flexbox-align-middle{
padding: 0;
}
.am-checkbox-agree{
margin: 0 !important;
}
.am-button,.am-checkbox-agree .am-checkbox-agree-label{
font-size: 28px !important;
}
}
}
.add-address-line{
.am-list-line:after{
border-bottom: 0 !important;
}
.am-list-line{
height: 100px;
}
}
.wx-address-add .am-list-body .am-list-item.am-list-item-middle.add-save-btn{
margin: 100px auto 20px auto !important;
border-bottom: 0 !important;
width: 568px;
height: 76px;
line-height: 76px;
padding-bottom: 40px !important;
}
.moren-btn{
height: 105px;
line-height: 105px;
border-bottom: 0 !important;
.am-list-line{
&::after{
border-bottom: none !important;
}
}
}
/*有的手机上没有线,改一下
.input-no-line{
.am-list-item.am-input-item:after{
border-bottom: 0;
}
.am-list-item.am-input-item{
border-bottom: 1px solid #ddd;
margin-left: 30px;
padding-left: 0;
}
}
.input-box-default{
margin: 0 40px;
padding: 50px 10px 10px 30px;
box-sizing: border-box;
border-bottom: 1px solid #ebebeb;
position: relative;
// height: 100px;
// line-height: 100px;
display: flex;
align-items: center;
i{
position: absolute;
font-style: normal;
color: #bd2d30;
font-weight: 700;
left: -11px;
}
span{
width: 175px;
height: 44px;
font-size: 32px;
font-weight: 400;
color: #666666;
line-height: 44px;
}
input{
box-shadow: none;
border: none;
flex: 1;
height: 44px;
line-height: 44px;
font-size: 32px;
font-weight: 400;
color: #333;
&::-webkit-input-placeholder{
font-size: 32px;
font-weight: 400;
color: #BDBFC0;
}
}
}
.save-address{
background: #fff;
.save-inputItem{
&.am-list-item.am-input-item{
padding-left: 0;
height: auto;
}
.am-input-label{
width: 175px;
height: 44px;
font-size: 32px;
font-weight: 400;
color: #666666;
line-height: 44px;
margin: 0;
}
.am-input-control{
height: 44px;
line-height: 44px;
input {
height: 44px;
line-height: 44px;
font-size: 32px;
font-weight: 400;
color: #333;
&::-webkit-input-placeholder{
font-size: 32px;
font-weight: 400;
color: #BDBFC0;
}
}
}
}
.add-pick{
padding-left: 0;
.am-list-line{
border-bottom: 1px solid #ebebeb;
.am-list-arrow{
height: 40px;
}
.am-list-content{
width: 175px;
height: 44px;
font-size: 32px;
font-weight: 400;
color: #666666;
line-height: 44px;
padding: 0;
flex: initial;
}
.am-list-extra{
flex: 1;
text-align: left;
height: 44px;
line-height: 44px;
font-size: 32px;
font-weight: 400;
color:#333;
}
}
}
.am-list-line{
margin: 0 40px;
padding: 50px 10px 10px 30px;
&:after{
background-color: #ebebeb !important;
transform: initial !important;
}
}
}
.save-title{
height: 50px;
font-size: 36px;
font-weight: 500;
color: #333333;
line-height: 50px;
text-align: center;
padding: 100px 0 10px;
}
.save-text{
padding-top: 0;
padding-top: 30px;
padding-bottom: 30px;
textarea{
border: 0;
outline: none;
resize: none;
flex: 1;
height: 80px;
line-height: 41px;
font-size: 32px;
color: #333;
}
}
.set-def{
margin: 0 40px;
padding: 40px 10px 10px 30px;
display: flex;
align-items: center;
justify-content: space-between;
span{
height: 44px;
font-size: 32px;
font-weight: 400;
color: #666666;
line-height: 44px;
}
.am-switch{
.checkbox{
width: 102px;
height: 62px;
&:before{
width: 93px;
height: 54px;
top: 4px;
left: 4px;
}
&:after{
width: 54px;
height: 54px;
top: 4px;
left: 4px;
}
}
input[type="checkbox"]:checked + .checkbox:after{
transform: translateX(73%);
}
input[type="checkbox"]:disabled + .checkbox{
opacity: 1;
}
}
}
.add-save-btn{
width: 610px;
margin: 130px auto 0;
.comm-btn{
height: 84px;
width: 100%;
border-radius: 84px;
font-size: 32px;
font-weight: 500;
}
}
.my-pick{
.am-picker-popup-item{
font-size: 30px;
height: 84px;
}
.am-picker-col-indicator{
font-size: 58px;
}
}
.act-add-list{
padding-bottom: 1.2rem;
.am-list-item{
padding: 0 30px;
.am-list-line-multiple{
padding: 0;
}
.am-list-line::after{
transform: initial !important;
}
}
.am-button-small{
height: .8rem;
line-height: .8rem;
padding: 0 .4rem;
background: rgba(21,80,73,.8);
border-radius: 0;
}
.am-button-primary{
background: rgba(21,80,73,.8);
font-size: .4rem;
}
.am-checkbox-agree .am-checkbox{
width: .8rem;
}
.am-checkbox-agree .am-checkbox-agree-label{
margin-left: .8rem;
}
.am-checkbox-inner{
width: .56rem;
height: .56rem;
border-width: .02rem;
top: 50%;
margin-top: -.28rem;
&:after{
width: .13333rem!important;
height: .29333rem!important;
border-width: 0 .04rem .04rem 0!important;
top: .04rem;
right: .16rem;
border-color: #108ee9;
}
}
}
*/
三、效果展示
上效果啦!!!当当当当~~~
转载自:https://juejin.cn/post/7051464384186417183