likes
comments
collection
share

仿京东地址选择组件

作者站长头像
站长
· 阅读数 22

本篇文章记录我在项目期间做的一个地址选择组件的优化。

一、先看下原始功能,上图

仿京东地址选择组件 三级地址数据全部一次性请求出来,接口响应时间共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
评论
请登录