zl程序教程

您现在的位置是:首页 >  其他

当前栏目

react + zarm 实现账单列表类型以及时间条件弹窗封装

2023-03-14 22:45:00 时间

需要实现的效果


点击类型,出现下面的条件弹窗

ad7985600f3840ac84feff3a8a7755f6.png


点击时间,出现下面的弹窗

2dbc24b3c1784661a04b9753e6b20fea.png



实现过程

这里用到 popup 组件 https://zarm.design/#/components/popup


1.封装类型条件组件

新建 components/PopupType,在其内部新建 index.jsxstyle.module.less 内容如下:


import React, { forwardRef, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { Popup, Icon } from 'zarm'
import cx from 'classnames'
import { queryTypeList } from './api/index.js'

import s from './style.module.less'

// forwardRef 用于拿到父组件传入的 ref 属性,这样在父组件便能通过 ref 控制子组件。
const PopupType = forwardRef(({ onSelect }, ref) => {
  const [show, setShow] = useState(false); // 组件的显示和隐藏
  const [active, setActive] = useState('all'); // 激活的 type
  const [expense, setExpense] = useState([]); // 支出类型标签
  const [income, setIncome] = useState([]); // 收入类型标签

  useEffect(async () => {
    // 请求标签接口放在弹窗内,这个弹窗可能会被复用,所以请求如果放在外面,会造成代码冗余。
    const { data } = await queryTypeList({});
    console.log(data);
    setExpense(data.filter(i => i.type == 1))
    setIncome(data.filter(i => i.type == 2))
  }, [])

  if (ref) {
    ref.current = {
      // 外部可以通过 ref.current.show 来控制组件的显示
      show: () => {
        setShow(true)
      },
      // 外部可以通过 ref.current.close 来控制组件的显示
      close: () => {
        setShow(false)
      }
    }
  };

  // 选择类型回调
  const choseType = (item) => {
    setActive(item.id)
    setShow(false)
    // 父组件传入的 onSelect,为了获取类型
    onSelect(item)
  };

  return <Popup
    visible={show}
    direction="bottom"
    onMaskClick={() => setShow(false)}
    destroy={false}
    mountContainer={() => document.body}
  >
    <div className={s.popupType}>
      <div className={s.header}>
        请选择类型
        <Icon type="wrong" className={s.cross} onClick={() => setShow(false)} />
      </div>
      <div className={s.content}>
        <div onClick={() => choseType({ id: 'all' })} className={cx({ [s.all]: true, [s.active]: active == 'all' })}>全部类型</div>
        <div className={s.title}>支出</div>
        <div className={s.expenseWrap}>
          {
            expense.map((item, index) => <p key={index} onClick={() => choseType(item)} className={cx({[s.active]: active == item.id})} >{ item.name }</p>)
          }
        </div>
        <div className={s.title}>收入</div>
        <div className={s.incomeWrap}>
          {
            income.map((item, index) => <p key={index} onClick={() => choseType(item)} className={cx({[s.active]: active == item.id})} >{ item.name }</p>)
          }
        </div>
      </div>
    </div>
  </Popup>
});

PopupType.propTypes = {
  onSelect: PropTypes.func
}

export default PopupType;


.popup-type {
    height: 500px;
    background-color: #f5f5f5;
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
    .header {
      position: sticky;
      top: 0;
      left: 0;
      z-index: 1000;
      width: 100%;
      height: 56px;
      text-align: center;
      font-size: 14px;
      line-height: 56px;
      color: rgba(0, 0, 0, 0.9);
      background-color: #fff;
      .cross {
        position: absolute;
        right: 10px;
        top: 50%;
        font-size: 20px;
        transform: translateY(-50%);
        color: rgba(0, 0, 0, 0.6);
      }
    }
    .content {
      padding: 20px;
      .all {
        display: inline-block;
        padding: 12px 20px;
        font-size: 16px;
        color: rgba(0, 0, 0, 0.9);
        background-color: #fff;
      }
      .title {
        color: rgba(0, 0, 0, 0.9);
        margin: 10px 0;
        font-size: 14px;
      }
      .expense-wrap, .income-wrap {
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
        p {
          width: calc(~"(100% - 20px) / 3");
          text-align: center;
          padding: 12px 0;
          margin-bottom: 10px;
          background-color: #fff;
          font-size: 16px;
        }
      }
      .active {
        background-color: #007fff!important;
        color: #fff;
      }
    }
  }



然后新建 components/PopupType/api,在其内部新建 index.js 添加如下:

import { fetchData } from "@/utils/axios.js";

// 获取类型字典列表
export function queryTypeList(data) {
  return fetchData('/api/type/list', 'get', data);
}


2.封装时间条件组件

新建 components/PopupDate,在其内部新建 index.jsx 代码如下:

import React, { forwardRef, useState } from 'react'
import PropTypes from 'prop-types'
import { Popup, DatePicker  } from 'zarm'
import dayjs from 'dayjs' 

const PopupDate = forwardRef(({ onSelect, mode = 'date' }, ref) => {
  const [show, setShow] = useState(false)
  const [now, setNow] = useState(new Date())

  const choseMonth = (item) => {
    setNow(item)
    setShow(false)
    if (mode == 'month') {
      onSelect(dayjs(item).format('YYYY-MM'))
    } else if (mode == 'date') {
      onSelect(dayjs(item).format('YYYY-MM-DD'))
    }
  }

  if (ref) {
    ref.current = {
      show: () => {
        setShow(true)
      },
      close: () => {
        setShow(false)
      }
    }
  };
  return <Popup
    visible={show}
    direction="bottom"
    onMaskClick={() => setShow(false)}
    destroy={false}
    mountContainer={() => document.body}
  >
    <div>
      <DatePicker
        visible={show}
        value={now}
        mode={mode}
        onOk={choseMonth}
        onCancel={() => setShow(false)}
      />
    </div>
  </Popup>
});

PopupDate.propTypes = {
  mode: PropTypes.string, // 日期模式
  onSelect: PropTypes.func, // 选择后的回调
}

export default PopupDate;


3.账单列表组件改动

import React, { useState, useEffect, useRef } from 'react'
import { Icon, Pull } from 'zarm'
import dayjs from 'dayjs'
import BillItem from '@/components/BillItem'
import PopupType from '@/components/PopupType'
import PopupDate from '@/components/PopupDate'
import { queryBillList } from './api/index.js'
import { REFRESH_STATE, LOAD_STATE } from '@/utils/index.js' // Pull 组件需要的一些常量

import s from './style.module.less'

const Home = () => {
  const typeRef = useRef(); // 账单类型 ref
  const monthRef = useRef(); // 月份筛选 ref
  const [currentSelect, setCurrentSelect] = useState({}); // 当前筛选类型
  const [currentTime, setCurrentTime] = useState(dayjs().format('YYYY-MM')); // 当前筛选时间
  const [totalExpense, setTotalExpense] = useState(0); // 总支出
  const [totalIncome, setTotalIncome] = useState(0); // 总收入
  const [page, setPage] = useState(1); // 分页
  const [dataList, setDataList] = useState([]); // 账单列表
  const [totalPage, setTotalPage] = useState(0); // 分页总数
  const [refreshing, setRefreshing] = useState(REFRESH_STATE.normal); // 下拉刷新状态
  const [loading, setLoading] = useState(LOAD_STATE.normal); // 上拉加载状态

  useEffect(() => {
    getBillList() // 初始化
  }, [page, currentSelect, currentTime])

  // 获取账单方法
  const getBillList = async () => {
    const { data } = await queryBillList({
      curPage: page,
      pageSize: 5,
      typeId: currentSelect.id || "all",
      billDate: currentTime
    });
    // 下拉刷新,重制数据
    if (page == 1) {
      setDataList(data.dataList);
    } else {
      setDataList(dataList.concat(data.dataList));
    }
    setTotalExpense(data.totalExpense);
    setTotalIncome(data.totalIncome);
    setTotalPage(data.pageObj.totalPage);
    // 上滑加载状态
    setLoading(LOAD_STATE.success);
    setRefreshing(REFRESH_STATE.success);
  }

  // 请求列表数据
  const refreshData = () => {
    setRefreshing(REFRESH_STATE.loading);
    if (page != 1) {
      setPage(1);
    } else {
      getBillList();
    };
  };

  const loadData = () => {
    if (page < totalPage) {
      setLoading(LOAD_STATE.loading);
      setPage(page + 1);
    }
  }

  // 添加账单弹窗
  const toggle = () => {
    typeRef.current && typeRef.current.show()
  };
  // 选择月份弹窗
  const monthToggle = () => {
    monthRef.current && monthRef.current.show()
  };

  // 筛选类型
  const select = (item) => {
    setRefreshing(REFRESH_STATE.loading);
    setPage(1);
    setCurrentSelect(item)
  }
  // 筛选月份
  const selectMonth = (item) => {
    setRefreshing(REFRESH_STATE.loading);
    setPage(1);
    setCurrentTime(item)
  }

  return <div className={s.home}>
    <div className={s.header}>
      <div className={s.dataWrap}>
        <span className={s.expense}>总支出:<b>¥ { totalExpense }</b></span>
        <span className={s.income}>总收入:<b>¥ { totalIncome }</b></span>
      </div>
      <div className={s.typeWrap}>
        <div className={s.left} onClick={toggle}>
          <span className={s.title}>{ currentSelect.name || '全部类型' } <Icon className={s.arrow} type="arrow-bottom" /></span>
        </div>
        <div className={s.right} onClick={monthToggle}>
          <span className={s.time}>{ currentTime } <Icon className={s.arrow} type="arrow-bottom" /></span>
        </div>
      </div>
    </div>
    <div className={s.contentWrap}>
      {
        dataList.length ? <Pull
          animationDuration={200}
          stayTime={400}
          refresh={{
            state: refreshing,
            handler: refreshData
          }}
          load={{
            state: loading,
            distance: 200,
            handler: loadData
          }}
        >
          {
            dataList.map((item, index) => <BillItem
              bill={item}
              key={index}
            />)
          }
        </Pull> : <div className={s.noData}>暂无账单数据</div>
      }
      <PopupType ref={typeRef} onSelect={select} />
      <PopupDate ref={monthRef} mode="month" onSelect={selectMonth} />
    </div>
  </div>
}

export default Home


样式添加了没有数据的情况

.no-data {
  text-align: center;
  font-size: 12px;
  padding: 10px 0;
}


4.测试

当前这个月没有数据,暂时如下:


bd88013beccf475f83435f863e7dcd27.png



点击时间,选择到2022年的2月份

357c1a0c6aaf4bf78cb9fcadb4c36e04.png


发现就有数据了:


5409a0616dde441a97d23456735e3894.png

切换类型到学习:

2d563759691747d8857fd51074f13739.png


选中之后:

2da8b623406b47bca7ecae5379ccc87f.png