react + zarm 实现账单列表展示页

简介: react + zarm 实现账单列表展示页

需要实现的效果


816a7dacb5904c60b7abf802f4d3f57a.png


实现过程


1.安装 dayjs

npm i dayjs -S

6a8aeb2f4a6342c1bdc49465fd3ce63d.png


https://dayjs.fenxianglu.cn/category/display.html#%E6%A0%BC%E5%BC%8F%E5%8C%96


c25f5a11755d4a129a6228cfd813b5a1.png


2.配置接口

找到账单列表接口


cf9371169dac4174b5d79bd90814f126.png


我们在 src\container\Home\api\index.js 里添加下面代码

import { fetchData } from "@/utils/axios.js";
// 获取账单列表
export function queryBillList(data) {
  return fetchData('/api/bill/list', 'get', data);
}


接口返回的数据结构如下:

http://localhost:3000/api/bill/list?r=663.1504597032896&curPage=3&pageSize=5&typeId=all&billDate=2022-02


2f2ff3ffab8f47298363eeb93a07841f.png


3.准备账单类型图标

我们去 iconfont 里找一些对应的图标,然后添加到我们项目的字体图标,然后更新链接


98c7893d54e4480f88e6a9652fd0ac54.png



4.配置下拉刷新以及类型的字典

我们在 src\utils\index.js 里添加下面配置代码

// 刷新状态
export const REFRESH_STATE = {
  normal: 0, // 普通
  pull: 1, // 下拉刷新(未满足刷新条件)
  drop: 2, // 释放立即刷新(满足刷新条件)
  loading: 3, // 加载中
  success: 4, // 加载成功
  failure: 5, // 加载失败
};
// 加载状态
export const LOAD_STATE = {
  normal: 0, // 普通
  abort: 1, // 中止
  loading: 2, // 加载中
  success: 3, // 加载成功
  failure: 4, // 加载失败
  complete: 5, // 加载完成(无新数据)
};
// 类型
export const typeMap = {
  1: {
    icon: 'canyin'
  },
  2: {
    icon: 'fushi'
  },
  3: {
    icon: 'jiaotong'
  },
  4: {
    icon: 'riyong'
  },
  5: {
    icon: 'gouwu'
  },
  6: {
    icon: 'xuexi'
  },
  7: {
    icon: 'yiliao'
  },
  8: {
    icon: 'lvxing'
  },
  9: {
    icon: 'renqing'
  },
  10: {
    icon: 'qita'
  },
  11: {
    icon: 'gongzi'
  },
  12: {
    icon: 'jiangjin'
  },
  13: {
    icon: 'zhuanzhang'
  },
  14: {
    icon: 'licai'
  },
  15: {
    icon: 'tuikuang'
  },
  16: {
    icon: 'qita'
  }
}


5.编写账单页

这里我们用到了 Pull 组件,用法可以查看 https://zarm.design/#/components/pull

14ac6b2e19f04c99961e86beb5cafa8b.png



我们在 src\container\Home\index.jsx 添加下面代码:(这里我写死了 billDate: “2022-02”,为了测试用的,这个条件有数据,后面会删掉)


import React, { useState, useEffect } from 'react'
import { Icon, Pull } from 'zarm'
import dayjs from 'dayjs'
import BillItem from '@/components/BillItem'
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 [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])
  // 获取账单方法
  const getBillList = async () => {
    const { data } = await queryBillList({
      curPage: page,
      pageSize: 5,
      typeId: "all",
      billDate: "2022-02" || 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);
    }
  }
  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}>
          <span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span>
        </div>
        <div className={s.right}>
          <span className={s.time}>2022-05<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> : null
      }
    </div>
  </div>
}
export default Home


src\container\Home\style.module.less 添加样式

.home {
    height: 100%;
    display: flex;
    flex-direction: column;
    padding-top: 80px;
    .header {
      position: fixed;
      top: 0;
      left: 0;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      width: 100%;
      height: 80px;
      background-color: #007fff;
      color: #fff;
      font-size: 14px;
      z-index: 100;
      padding: 10px;
      .data-wrap {
        font-size: 14px;
        > span {
          font-size: 12px;
          > b {
            font-size: 26px;
            font-family: DINCondensed-Bold, DINCondensed;
            margin-left: 4px;
          }
        }
        .income {
          margin-left: 10px;
        }
      }
      .type-wrap {
        display: flex;
        justify-content: flex-end;
        align-items: flex-end;
        > div {
          align-self: flex-start;
          background: rgba(0, 0, 0, 0.1);
          border-radius: 30px;
          padding: 3px 8px;
          font-size: 12px;
        }
        .left {
          margin-right: 6px;
        }
        .arrow {
            font-size: 12px;
            margin-left: 4px;
        }
      }
    }
    .content-wrap {
      height: calc(~"(100% - 50px)");
      overflow: hidden;
      overflow-y: scroll;
      background-color: #f5f5f5;
      padding: 10px;
      :global {
        .za-pull {
          overflow: unset;
        }
      }
    }
  }


6.编写账单BillItem子组件


下面需要实现

c3e0e6d774ce44f2ac1127c456604162.png


src\components\BillItem\index.jsx 添加代码

import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { Cell } from 'zarm';
import { useNavigate } from 'react-router-dom'
import CustomIcon from '../CustomIcon';
import { typeMap } from '@/utils';
import s from './style.module.less';
const BillItem = ({ bill }) => {
  const [income, setIncome] = useState(0); // 收入
  const [expense, setExpense] = useState(0); // 支出
  const navigate = useNavigate(); // 路由实例
  // 当添加账单是,bill.bills 长度变化,触发当日收支总和计算。
  useEffect(() => {
    // 初始化将传入的 bill 内的 bills 数组内数据项,过滤出支出和收入。
    // pay_type:1 为支出;2 为收入
    // 通过 reduce 累加
    const _income = bill.bills.filter(i => i.pay_type == 2).reduce((curr, item) => {
      curr += Number(item.amount);
      return curr;
    }, 0);
    setIncome(_income);
    const _expense = bill.bills.filter(i => i.pay_type == 1).reduce((curr, item) => {
      curr += Number(item.amount);
      return curr;
    }, 0);
    setExpense(_expense);
  }, [bill.bills]);
  // 前往账单详情
  const goToDetail = (item) => {
    navigate(`/detail?id=${item.id}`)
  };
  return <div className={s.item}>
    <div className={s.headerDate}>
      <div className={s.date}>{bill.day}</div>
      <div className={s.money}>
        <span>
          <img src={`src/assets/images/zhi.png`} alt='支' />
            <span>¥{ expense.toFixed(2) }</span>
        </span>
        <span>
          <img src={`src/assets/images/shou.png`} alt="收" />
          <span>¥{ income.toFixed(2) }</span>
        </span>
      </div>
    </div>
    {
      bill && bill.bills.map(item => <Cell
        className={s.bill}
        key={item.id}
        onClick={() => goToDetail(item)}
        title={
          <>
            <CustomIcon
              className={s.itemIcon}
              type={item.type_id ? typeMap[item.type_id].icon : 1}
            />
            <span>{ item.type_name }</span>
          </>
        }
        description={<span style={{ color: item.pay_type == 2 ? 'red' : '#39be77' }}>{`${item.pay_type == 1 ? '-' : '+'}${item.amount}`}</span>}
        help={<div>{dayjs(item.date).format('HH:mm')} {item.remark ? `| ${item.remark}` : ''}</div>}
      >
      </Cell>)
    }
  </div>
};
BillItem.propTypes = {
  bill: PropTypes.object
};
export default BillItem;


src\components\BillItem\style.module.less 里添加子组件的样式

.item {
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.1);
    margin-bottom: 10px;
    .header-date {
      height: 40px;
      display: flex;
      background-color: #f9f9f9;
      align-items: center;
      justify-content: space-between;
      padding: 0 10px;
      div {
        color: rgba(0, 0, 0, 0.9);
      }
      .date {
        font-weight: bold;
        font-size: 16px;
      }
      .money {
        > span {
          margin-left: 20px;
          img {
            width: 20px;
            margin-right: 4px;
            vertical-align: -4px
          }
        }
      }
    }
    :global {
      .za-cell {
        &::after {
          left: 0;
        }
        .za-cell__title {
          display: flex;
          align-items: center;
        }
      }
    }
    .item-icon {
      font-size: 24px;
      color: #007fff;
      vertical-align: -2px;
      margin-right: 2px;
    }
  }


7.修改 index.css 文件代码

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
body, html, p {
  height: 100%;
  margin: 0;
  padding: 0;
}
* {
  box-sizing: border-box;
}
#root {
  height: 100%;
}
.text-deep {
  color: rgba(0, 0, 0, 0.9)
}
.text-light {
  color: rgba(0, 0, 0, 0.6)
}



8.测试

测试效果如下:

da923bf8249b45aeba828d4062442443.png



Flex 布局学习网站

http://flexboxfroggy.com/#zh-cn

c412cc8a459549e7b0350107b40998fd.png




目录
相关文章
|
前端开发 算法 JavaScript
React-组件-内联样式 和 React-组件-列表渲染优化
React-组件-内联样式 和 React-组件-列表渲染优化
93 0
|
1月前
|
前端开发 JavaScript API
React 列表 & Keys
10月更文挑战第9天
14 0
|
1月前
|
JavaScript 前端开发 算法
写 React / Vue 项目时为什么要在列表组件中写 key
在React或Vue项目中,为列表组件中的每个元素添加唯一的key属性,有助于框架高效地更新和渲染列表。Key帮助虚拟DOM识别哪些项已更改、添加或删除,从而优化性能并减少不必要的重新渲染。
|
4月前
|
前端开发
react18【实战】tab切换,纯前端列表排序(含 lodash 和 classnames 的安装和使用)
react18【实战】tab切换,纯前端列表排序(含 lodash 和 classnames 的安装和使用)
49 1
|
6月前
|
存储 JavaScript 前端开发
基于React和Redux的待办事项列表应用
基于React和Redux的待办事项列表应用
62 0
|
6月前
|
存储 前端开发
构建一个简单的React待办事项列表应用
构建一个简单的React待办事项列表应用
55 0
|
6月前
|
前端开发 JavaScript
【边做边学】利用React创建交互式ToDo列表
【边做边学】利用React创建交互式ToDo列表
|
6月前
|
前端开发
react 商品列表返回顶部
react 商品列表返回顶部
|
前端开发
前端项目实战玖拾陆react-admin+material ui-踩坑-List的用法之Empty来设置空列表
前端项目实战玖拾陆react-admin+material ui-踩坑-List的用法之Empty来设置空列表
77 0
|
前端开发
前端项目实战叁拾肆-​react-admin+material ui-单表订单列表新增
前端项目实战叁拾肆-​react-admin+material ui-单表订单列表新增
65 0