react + zarm + antV F2 实现账单数据统计饼图效果

Posted 凯小默:树上的男爵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react + zarm + antV F2 实现账单数据统计饼图效果相关的知识,希望对你有一定的参考价值。

需要实现的效果

为了方便展示,饼图放到右边标明:

实现过程

这里我们尝试用一下 antV F2 移动端可视化引擎来实现饼图效果

1.F2 移动端可视化引擎

F2 是一个专注于移动端,面向常规统计图表,开箱即用的可视化引擎,完美支持 H5 环境同时兼容多种环境(Node, 小程序),完备的图形语法理论,满足你的各种可视化需求,专业的移动设计指引为你带来最佳的移动端图表体验。

如何在 React 中使用:

npm install @antv/f2 --save
npm install @antv/f2-react --save

我们可以参考这里的例子:

2.封装饼图组件

我们在 components 里添加 PieChart 文件夹,里面新增 index.jsxstyle.module.less 文件,添加代码如下:

import Canvas from "@antv/f2-react";
import  Chart, Interval, Legend, PieLabel  from "@antv/f2";
import PropTypes from 'prop-types';

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

const PieChart = ( chartData = [] ) => 
  console.log('进入 PieChart', chartData)
  return (
    <div className=s.pieChart>
       chartData.length > 0 ? <Canvas pixelRatio=window.devicePixelRatio>
        <Chart
          data=chartData
          coord=
            type: "polar",
            transposed: true,
            radius: 1,
          
          scale=
        >
          <Interval
            x="payType"
            y="percent"
            adjust="stack"
            color=
              field: "type_name",
              range: ['#5a71c1', '#9eca7e', '#f3ca6b', '#df6e6b', '#84bedb', '#589f74', '#ed8a5c', '#1e80ff', '#fc5531', '#67c23a'],
            
            selection=
              selectedStyle: (record) => 
                const  yMax, yMin  = record;
                return 
                  // 半径放大 1.1 倍
                  r: (yMax - yMin) * 1.1,
                ;
              ,
            
          />
          <Legend position="top" marker="square" nameStyle=
            fontSize: '14',
            fill: '#000',
           style=
            justifyContent: 'space-between',
            flexDirection: 'row',
            flexWrap: 'wrap'
          />
          <PieLabel
            sidePadding=0
            label1=(data) => 
              return 
                text: `$data.type_name:$data.percent%`,
                fill: "#0d1a26",
                fontSize: 12,
              ;
            
          />
        </Chart>
      </Canvas> : null 
    </div>
  );
;

PieChart.propTypes = 
  chartData: PropTypes.array,


export default PieChart;
.pie-chart 
  min-height: 200px;

3.编写数据分析页面的逻辑

我们在 container 里添加 Data 文件夹,里面新增 index.jsxstyle.module.less 文件,以及 api 相关配置文件,添加代码如下:

import React,  useEffect, useRef, useState  from 'react';
import  Icon, Progress, Toast  from 'zarm';
import cx from 'classnames';
import dayjs from 'dayjs';
import CustomIcon from '@/components/CustomIcon';
import PopupDate from '@/components/PopupDate';
import PieChart from '@/components/PieChart';
import  analysisMonthBill  from "./api/index.js";
import  typeMap  from '@/utils';

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

const Data = () => 
  const monthRef = useRef();
  const [currentMonth, setCurrentMonth] = useState(dayjs().format('YYYY-MM')); // 当前月份
  const [totalType, setTotalType] = useState('expense'); // 收入或支出类型
  const [totalExpense, setTotalExpense] = useState(0); // 总支出
  const [totalIncome, setTotalIncome] = useState(0); // 总收入
  const [expenseData, setExpenseData] = useState([]); // 支出数据
  const [incomeData, setIncomeData] = useState([]); // 收入数据
  const [pieType, setPieType] = useState('expense'); // 饼图的「收入」和「支出」控制
  const [chartData, setChartData] = useState([]); // 饼图需要渲染的数据

  useEffect(() => 
    getData();
  , [currentMonth]);

  // 获取数据详情
  const getData = async () => 
    const  status, desc, data  = await analysisMonthBill(
      billDate: currentMonth // 示例值:2022-02
    );
    console.log('获取数据详情', status, desc, data)

    if(status === 200) 
      // 总收支
      setTotalExpense(data.totalExpense);
      setTotalIncome(data.totalIncome);
    
      // 过滤支出和收入
      let expense_data = data.dataList.filter(item => item.pay_type == 1).sort((a, b) => b.number - a.number); // 过滤出账单类型为支出的项
      let income_data = data.dataList.filter(item => item.pay_type == 2).sort((a, b) => b.number - a.number); // 过滤出账单类型为收入的项
      expense_data = expense_data.map(item => 
        return 
          ...item,
          payType: item.pay_type.toString(),
          percent: Number(Number((item.number / Number(data.totalExpense)) * 100).toFixed(2))
        
      )
      income_data = income_data.map(item => 
        return 
          ...item,
          payType: item.pay_type.toString(),
          percent: Number(Number((item.number / Number(data.totalIncome)) * 100).toFixed(2))
        
      )
      
      setExpenseData(expense_data);
      setIncomeData(income_data);
      // 设置饼图数据
      setChartData(pieType == 'expense' ? expense_data : income_data);
    else
      Toast.show(desc);
    
  ;

  // 月份弹窗开关
  const monthShow = () => 
    monthRef.current && monthRef.current.show();
  ;
  // 选择月份
  const selectMonth = (item) => 
    setCurrentMonth(item);
  ;
  // 切换收支构成类型
  const changeTotalType = (type) => 
    setTotalType(type);
  ;
  // 切换饼图收支类型
  const changePieType = (type) => 
    setPieType(type);
    setChartData(type == 'expense' ? expenseData : incomeData);
  

  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>¥ totalExpense </div>
      <div className=s.income>共收入¥ totalIncome </div>
    </div>
    <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 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>
        /* 饼图 */
        <PieChart chartData=chartData />
      </div>
    </div>
    <PopupDate ref=monthRef mode="month" onSelect=selectMonth />
  </div>


export default Data
.data 
  min-height: 100%;
  background-color: #f5f5f5;

  .total 
    background-color: #fff;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 24px 0;
    margin-bottom: 10px;

    .time 
      position: relative;
      width: 96px;
      padding: 6px;
      background-color: #f5f5f5;
      color: rgba(0, 0, 0, .9);
      border-radius: 4px;
      font-size: 14px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 16px;

      span:nth-of-type(1)::after 
        content: '';
        position: absolute;
        top: 9px;
        bottom: 8px;
        right: 28px;
        width: 1px;
        background-color: rgba(0, 0, 0, .5);
      

      .date 
        font-size: 16px;
        color: rgba(0, 0, 0, .5);
      
    

    .title 
      color: #007fff;
      margin-bottom: 8px;
      font-size: 12px;
      font-weight: 500;
    

    .expense 
      font-sizereact + zarm 实现账单列表展示页

react + zarm 实现底部导航栏

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

react + zarm 实现账单详情页以及编辑删除功能

AntV中F2在微信小程序中的使用

react + zarm + rc-form + crypto-js 实现个人中心页面,头像上传,密码重置,登录退出功能