前言
账单的操作部分在之前的章节已经结束了,本章节我们学习如何将账单列表,以可视化数据的新形势展示,本章节我们会通过 Echart
插件,对数据进行可视化展示。
页面布局和分析如下所示:
知识点
- Echart 引入和使用。
- 进度条组件
Progress
的使用。
正文
头部筛选和数据实现
当你看到顶部的时间筛选项的时候,你会再一次体会到当初把时间筛选功能封装成公用组件的好处,于是我们打开 Data/index.jsx
,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import React, { useEffect, useRef, useState } from "react"; import { Icon, Progress } from "zarm"; import cx from "classnames"; import dayjs from "dayjs"; import { get, typeMap } from "@/utils"; import CustomIcon from "@/components/CustomIcon"; import PopupDate from "@/components/PopupDate"; import s from "./style.module.less";
const Data = () => { return ( <div className={s.data}> <div className={s.total}> <div className={s.time}> <span>2021-06</span> <Icon className={s.date} type="date" /> </div> <div className={s.title}>共支出</div> <div className={s.expense}>¥1000</div> <div className={s.income}>共收入¥200</div> </div> </div> ); };
export default Data;
|
头部的一些引入是为后面的代码所用,在这里事先引入,避免后面重复出现。
上述代码为头部统计的页面布局,同样采用的 flex
布局,样式部分如下:
文末已为同学们提供下本章节 demo 代码,样式部分不再详细说明。
样式部分有一个小技巧需要注意,日期后面的小竖线,如下所示:
在业务中,类似这样的需求非常多,这里我们可以使用伪类 ::before
或 ::after
去实现,减少在页面中再添加一些多余的标签。上述代码实现的逻辑是在日期的 span
上加上 ::after
,如下所示:
1 2 3 4 5 6 7 8 9
| span:nth-of-type(1)::after { content: ""; position: absolute; top: 9px; bottom: 8px; right: 28px; width: 1px; background-color: rgba(0, 0, 0, 0.5); }
|
给一个空的 content
,再给上 1px
的宽度,颜色和上下距离可以根据需求调整。
苦口婆心的我再次强调,flex
布局的重要性,一定要把它吃透,至少在布局的时候,你可以灵活的运用横竖布局。
点击如期按钮,弹出底部弹窗,这里使用到了之前写好的 PopupDate
组件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const Data = () => { const monthRef = useRef(); const [currentMonth, setCurrentMonth] = useState(dayjs().format("YYYY-MM"));
const monthShow = () => { monthRef.current && monthRef.current.show(); };
const selectMonth = (item) => { setCurrentMonth(item); };
return ( <div className={s.data}> <div className={s.total}> <div className={s.time} onClick={monthShow}> <span>{currentMonth}</span> <Icon className={s.date} type="date" /> </div> <div className={s.title}>共支出</div> <div className={s.expense}>¥1000</div> <div className={s.income}>共收入¥200</div> </div> <PopupDate ref={monthRef} mode="month" onSelect={selectMonth} /> </div> ); };
|
给日期按钮添加 monthShow
点击事件,调出 PopupDate
弹窗。并且,通过 selectMonth
方法,设置好选择的月份,展示于页面之上。效果如下所示:
账单单项排名制作
我们将账单排名部分的结构搭建出来,通过请求数据接口,将数据展示在页面上,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| const Data = () => { ... const [totalType, setTotalType] = useState('expense'); const [totalExpense, setTotalExpense] = useState(0); const [totalIncome, setTotalIncome] = useState(0); const [expenseData, setExpenseData] = useState([]); const [incomeData, setIncomeData] = useState([]);
useEffect(() => { getData() }, [currentMonth]);
const getData = async () => { const { data } = await get(`/api/bill/data?date=${currentMonth}`);
// 总收支 setTotalExpense(data.total_expense); setTotalIncome(data.total_income);
// 过滤支出和收入 const expense_data = data.total_data.filter(item => item.pay_type == 1).sort((a, b) => b.number - a.number); // 过滤出账单类型为支出的项 const income_data = data.total_data.filter(item => item.pay_type == 2).sort((a, b) => b.number - a.number); // 过滤出账单类型为收入的项 setExpenseData(expense_data); setIncomeData(income_data); };
return <div className={s.data}> ... <div className={s.structure}> <div className={s.head}> <span className={s.title}>收支构成</span> <div className={s.tab}> <span onClick={() => changeTotalType('expense')} className={cx({ [s.expense]: true, [s.active]: totalType == 'expense' })}>支出</span> <span onClick={() => changeTotalType('income')} className={cx({ [s.income]: true, [s.active]: totalType == 'income' })}>收入</span> </div> </div> <div className={s.content}> { (totalType == 'expense' ? expenseData : incomeData).map(item => <div key={item.type_id} className={s.item}> <div className={s.left}> <div className={s.type}> <span className={cx({ [s.expense]: totalType == 'expense', [s.income]: totalType == 'income' })}> <CustomIcon type={item.type_id ? typeMap[item.type_id].icon : 1} /> </span> <span className={s.name}>{ item.type_name }</span> </div> <div className={s.progress}>¥{ Number(item.number).toFixed(2) || 0 }</div> </div> <div className={s.right}> <div className={s.percent}> <Progress shape="line" percent={Number((item.number / Number(totalType == 'expense' ? totalExpense : totalIncome)) * 100).toFixed(2)} theme='primary' /> </div> </div> </div>) } </div> </div> ... </div> }
|
上述是账单排名部分的代码部分,通过 getData
方法获取账单数据,接口字段分析:
首先我们需要传递日期参数 date
,才能正常获取该月份的数据。
- number: 账单金额。
- pay_type:账单类型,1 为支出,2 为收入。
- type_id:账单种类 id。
- type_name:账单种类名称,如购物、交通、医疗等。
并将数据进行二次处理,将「收入」和「支出」分成两个数组保存。
通过 changeTotalType
方法,切换展示「收入」或「支出」。
通过对 Progress
组件的样式二次修改,样式代码如下:
文末已为同学们提供下本章节 demo 代码,样式部分不再详细说明。
将组件展示效果改成如下所示:
饼图制作
接下来我们尝试引入 Echart
,我们不通过 npm
引入它,我们尝试引入它的静态资源,找到根目录下的 index.html
,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" sizes="32x32" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/favicons/v2/favicon-32x32.png~tplv-t2oaga2asx-image.image"> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>掘掘手札</title> </head> <body> <div id="root"></div> <script src="//s.yezgea02.com/1609305532675/echarts.js"></script> <script type="module" src="/src/main.jsx"></script> </body> </html>
|
这种引入方式,不会将 echart
打包到最终的入口脚本里。有同学会说可以按需引入,但是就算是按需引入,脚本也会变得很大,本身 echart
这类可视化工具库就非常大,因为内部使用了大量绘制图形的代码。
完成上述操作之后,我们尝试在 /Data/index.jsx
添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| let proportionChart = null;
const Data = () => { ... const [pieType, setPieType] = useState('expense');
useEffect(() => { getData(); return () => { proportionChart.dispose(); }; }, [currentMonth]);
const setPieChart = (data) => { if (window.echarts) { proportionChart = echarts.init(document.getElementById('proportion')); proportionChart.setOption({ tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' }, legend: { data: data.map(item => item.type_name) }, series: [ { name: '支出', type: 'pie', radius: '55%', data: data.map(item => { return { value: item.number, name: item.type_name } }), emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] }) }; };
const getData = async () => { setPieChart(pieType == 'expense' ? expense_data : income_data); };
const changePieType = (type) => { setPieType(type); setPieChart(type == 'expense' ? expenseData : incomeData); }
return <div className={s.data}> ... <div className={s.structure}> <div className={s.proportion}> <div className={s.head}> <span className={s.title}>收支构成</span> <div className={s.tab}> <span onClick={() => changePieType('expense')} className={cx({ [s.expense]: true, [s.active]: pieType == 'expense' })}>支出</span> <span onClick={() => changePieType('income')} className={cx({ [s.income]: true, [s.active]: pieType == 'income' })}>收入</span> </div> </div> {/* 这是用于放置饼图的 DOM 节点 */} <div id="proportion"></div> </div> </div> </div> }
|
切换饼图「收入」和「支出」这里,我使用了一个小技巧,每次调用 setPieChart
的时候,会将数据重新传入,此时的数据是经过 changePieType
接收的参数进行筛选的,如果形参 type
的值为 expense
,那么给 setPieChart
传的参数为 expenseData
,反之则为 incomeData
。
注意,在页面销毁前,需要将实例清除。在 useEffect
内 return
一个函数,该函数就是在组件销毁时执行,在函数内部执行 proportionChart.dispose();
对实例进行销毁操作。
最后,我们将头部的数据补上,如下所示:
1 2
| <div className="{s.expense}">¥{ totalExpense }</div> <div className="{s.income}">共收入¥{ totalIncome }</div>
|
浏览器展示如下:
总结
可视化的形式还有很多,相关的可视化库有 three.js、d3.js、highchart.js 等等,这些内容都值得你去深入,但是在此之前,希望同学们能明确自己希望深入那一方面的知识,进行深度学习。比如张鑫旭大神,对 CSS
的研究,入骨三分,我相信业务无人出其右。所以找准方向,往前冲。