仿Antd-mobile的Cascader实现省市区联动

11 篇文章 0 订阅

为啥不直接用Cascader 级联选择组件呢?主要是因为作为老项目,已经引入了antd-mobile@2.3.4,同时引入v5版本会有兼容性问题。

原始数据格式:

首先需要将后端返回的数据转为前端定义的格式,方便使用:

[
  {
    "label": "安徽省",
    "value": "340000",
    "children": [
      {
        "label": "安庆市",
        "value": "340800",
        "children": [
          {
            "label": "大观区",
            "value": "340803",
            "children": []
          },
          ...其他区
        ]
      },
      ...其他市
    ]
  },
  ...其他省份
]

树结构转数组结构:

研究了下antd-mobile的cascader-view源码,我发现精髓在于将树结构转换成了方便开发的数组:

  //选择的value 一维数组
  const [value, setValue] = useState([]);

  const levels = useMemo(() => {
    const ret = [];
    //当前列表
    let currentOptions = 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 || !target['children'] || isEmpty(target['children'])) {
        reachedEnd = true;
        break;
      }
      currentOptions = target['children'];
    }
    if (!reachedEnd) {
      ret.push({
        selected: undefined,
        options: currentOptions,
      });
    }
    return ret;
  }, [value]);

当未选择时levels结构:

[
  {
    //未选中
    selected: undefined,
    options: [
      {
        "label": "安徽省",
        "value": "340000",
        "children": [
          {
            "label": "安庆市",
            "value": "340800",
            "children": [
              {
                "label": "大观区",
                "value": "340803",
                "children": []
              },
              ...其他区
            ]
          },
          ...其他市
        ]
      },
      ...其他省份
    ]
  }
]

选中省份时levels结构:

[
  {
    //选中省份
    "selected": {
      "label": "安徽省",
      "value": "340000",
      "children": [
        {
          "label": "安庆市",
          "value": "340800",
          "children": [
            {
              "label": "大观区",
              "value": "340803",
              "children": []
            },
            ...其他区
          ]
        },
        ...其他市
      ]
    },  
    "options": [
      {
        "label": "安徽省",
        "value": "340000",
        "children": [
          {
            "label": "安庆市",
            "value": "340800",
            "children": [
              {
                "label": "大观区",
                "value": "340803",
                "children": []
              },
              ...其他区
            ]
          },
          ...其他市
        ]
      },
      ...其他省份
    ]
  },
  {
    //未选中
    "selected": undefined,
    "options": [
      {
        "label": "安庆市",
        "value": "340800",
        "children": [
          {
            "label": "大观区",
            "value": "340803",
            "children": []
          },
          ...其他区
        ]
      },
      ...其他市
    ]
  }
]

数据结构清楚以后,编码就相对简单了:

import PopShow from '@/components/PopShow';
import React, { useMemo, useState } from 'react';
//v2版本的
import { Tabs } from 'antd-mobile';
import styles from './index.less';
import { isEmpty } from 'lodash';
import { CheckOutline } from 'antd-mobile-icons';
import classNames from 'classnames';

const AddressModal = ({options}) => {

  const [value, setValue] = useState([]);

  const [page, setPage] = useState(0);

  //精髓在于这段代码,将树形结构转为数组
  const levels = useMemo(() => {
    const ret = [];
    //当前列表
    let currentOptions = 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 || !target['children'] || isEmpty(target['children'])) {
        reachedEnd = true;
        break;
      }
      currentOptions = target['children'];
    }
    if (!reachedEnd) {
      ret.push({
        selected: undefined,
        options: currentOptions,
      });
    }
    return ret;
  }, [value]);

  const tabs = useMemo(() => {
    const ret = levels?.map(level => {
      if (level?.selected) {
        return {
          title: level?.selected['label'],
        };
      }
      return {
        title: '请选择',
      };
    }) || [{
      title: '请选择',
    }];
    //滑动到下一tab
    setPage(ret?.length - 1);
    return ret;
  }, [levels]);

  const onItemSelect = (selectValue, depth) => {
    const next = value.slice(0, depth);
    if (selectValue !== undefined) {
      next[depth] = selectValue;
    }
    setValue(next);
  };

  return <PopShow visible={true}>
    <div className={styles.popShow}>
      <div className={styles.topButtons}>
        <span>取消</span>
        <span>确定</span>
      </div>
      <Tabs tabs={tabs} swipeable={false} page={page}
            onChange={(_, index) => setPage(index)}
            tabBarActiveTextColor={'#BB6532'}
            tabBarInactiveTextColor={'#000000'}
            tabBarUnderlineStyle={{ display: 'none' }}
            tabBarTextStyle={{ fontSize: '14px' }}
      >
        {
          levels?.map((level, index) => {
            const options = level?.options;
            return (
              <div className={styles.checklist}>
                {
                  options?.map(option => {
                    const active = value[index] === option['value'];
                    return <div onClick={() => onItemSelect(option['value'], index)}
                      className={classNames({[styles.active]: active})}
                    >
                      <span>{option['label']}</span>
                      {active && <CheckOutline />}
                    </div>;
                  })
                }
              </div>
            );
          })
        }
      </Tabs>
    </div>
  </PopShow>;
};

export default AddressModal;
@import (reference) '../../styles/index.less';

.popShow {
  height: 60vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;

  .topButtons {
    display: flex;
    justify-content: space-between;
    padding: 8*@rem 16*@rem 4*@rem;
    color: #343434;
  }

  .checklist {
    flex: 1;
    overflow-y: scroll;

    &::-webkit-scrollbar {
      display: none;
    }

    & > div {
      text-align: left;
      padding: 8*@rem 16*@rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .active{
      color: #BB6532;
    }
  }



  :global {
    .am-tabs-default-bar-tab {
      width: auto !important;
      padding-left: 16*@rem;
      padding-right: 16*@rem;
      max-width: 33.3%;
      .textEllipsis;
    }

    .am-tabs-default-bar-content {
      position: relative;

      &:after {
        content: '';
        position: absolute;
        background-color: #ddd;
        display: block;
        z-index: 1;
        top: auto;
        right: auto;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 1px;
      }
    }
  }
}

附上效果图:
QQ录屏20240704104232.gif

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: antd-mobile-vue是一个基于Vue.js框架的移动端组件库,它提供了一系列UI组件,用于开发高质量的移动端应用。 与其它UI组件库相比,antd-mobile-vue具有以下优势和特点: 1. 高质量的组件:antd-mobile-vue提供了丰富的移动端UI组件,如按钮、表单、弹窗、导航等,这些组件都经过了精心设计和开发,具有统一的风格和良好的用户体验。 2. 灵活的布局:antd-mobile-vue提供了灵活的布局组件,如栅格布局、Flex布局,可帮助开发者快速搭建页面结构,并自适应不同的屏幕尺寸。 3. 易于使用和扩展:antd-mobile-vue的组件使用简单,开发者可以通过简单的配置和参数就可以实现复杂的交互效果。而且,antd-mobile-vue的组件提供了丰富的扩展能力,可以根据项目需求进行个性化的定制。 4. 生态丰富:antd-mobile-vue拥有庞大的开发者社区和活跃的维护团队,开发者可以通过官方文档和社区资源获取帮助和支持。此外,antd-mobile-vue还与其它Vue.js生态工具和库良好地兼容,如Vue Router、Vue CLI等。 5. 支持国际化:antd-mobile-vue提供了多语言支持,开发者可以根据项目需求灵活地切换多种语言环境。 总之,antd-mobile-vue是一个功能强大、易于使用和扩展的移动端组件库,它可以帮助开发者快速构建高质量的移动端应用,提高开发效率和用户体验。 ### 回答2: antd-mobile-vue是一种基于Vue.js框架的移动端UI库。它是对Ant Design Mobile的Vue组件实现的封装和扩展,旨在为开发者提供高质量、易用性的移动端组件库,帮助快速开发移动应用程序。 antd-mobile-vue提供了丰富的移动端UI组件,如按钮、导航栏、标签栏、列表、表单等,可以满足日常开发中绝大部分的界面需求。这些组件都经过精心设计和优化,在视觉和交互上都符合当前移动端的设计原则和用户体验。而且,它还提供了灵活的定制和扩展能力,允许开发者根据具体需求进行个性化定制,提高开发效率和用户体验。 除了UI组件外,antd-mobile-vue还提供了一些实用的工具和功能,如样式工具库、语言国际化、路由管理等。这些工具和功能都是为了让开发者更方便地进行移动应用开发,减少重复性的工作,提高开发效率。 antd-mobile-vue拥有广泛的社区支持和文档资料,开发者可以从社区中获取帮助和解决问题,学习和掌握使用该库的技巧和最佳实践。同时,antd-mobile-vue还提供了详细的官方文档和示例代码,方便开发者快速入手和上手该库。 总之,antd-mobile-vue是一款功能强大、易用性强的移动端UI库,适用于各种移动应用的开发。无论是个人开发者还是团队开发,都可以通过使用antd-mobile-vue来快速构建高质量的移动应用程序。 ### 回答3: antd-mobile-vue 是一个基于 Vue.js 的移动端 UI 组件库,它提供了丰富的移动端组件和样式风格,可以帮助开发者快速构建优雅的移动端应用。 antd-mobile-vue 的特点有以下几个方面: 1. 高度可定制:antd-mobile-vue 提供了大量的组件,涵盖了移动端常见的UI元素,如按钮、导航栏、表单等,这些组件的样式和交互行为都可以通过配置进行定制,满足不同项目的需求。 2. 兼容性强:antd-mobile-vue 提供了对不同移动端浏览器和操作系统的支持,保证组件在不同环境下的正常运行和展示,同时也保证了用户的使用体验。 3. 特色设计:antd-mobile-vue 的设计风格简洁、现代,符合移动端用户的审美要求,同时也遵循了 Material Design 和 iOS Human Interface Guidelines 等设计准则,保证了用户的熟悉感和易用性。 4. 文档丰富:antd-mobile-vue 提供了详细的文档和示例代码,开发者可以根据文档了解组件的使用方法和配置参数,快速上手使用。 5. 生态丰富:antd-mobile-vue 是基于 Ant Design Mobileantd-mobile)的 Vue 实现,可以与其它 Vue 生态工具和插件无缝集成,如 Vue Router、Vuex 等,方便开发者构建复杂的移动应用。 总之,antd-mobile-vue 是一个强大而灵活的移动端 UI 组件库,它可以帮助开发者节省时间和精力,快速开发出高质量的移动应用。无论是个人项目还是企业应用,都可以考虑使用这个库来提升开发效率和用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值