需要实现的效果
实现过程
1.安装 dayjs
npm i dayjs -S
https://dayjs.fenxianglu.cn/category/display.html#%E6%A0%BC%E5%BC%8F%E5%8C%96
2.配置接口
找到账单列表接口
我们在 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
3.准备账单类型图标
我们去 iconfont 里找一些对应的图标,然后添加到我们项目的字体图标,然后更新链接
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
我们在 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子组件
下面需要实现
在 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.测试
测试效果如下:
Flex 布局学习网站
http://flexboxfroggy.com/#zh-cn