最近在学react,刚好写了一个记账本,和大家分享一下具体是如何实现的,以及里面的一些核心思路   仓库:https://gitee.com/zhangxinpingwangdexian/small-bill-of-lading.git

1.效果展示:

1.月度账单部分

2.记账功能

 

2.业务

该项目实现的核心功能就是可以让用户把自己每天的开销记到这个平台上,平台会做一个统计,并且会对用户的花销进行一个汇总(月度,年度),有利于金钱的管理

3.项目核心实现

3.1框架:
3.1.1.环境配置
npm i @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios
3.1.2.别名路径配置
---路径解析配置 (webpack), 把@/解析成src/ (craco插件)
CRA本身把webpack配置包装到了黑盒中无法直接修改,需要一个插件进行修改(craco)
配置步骤:
  1. 安装craco npm i -D @craco/craco
  2. 项目根目录下创建配置文件 craco.config.js(必须是这个名字)
  3. 配置文件中添加路径解析配置
  4. 包文件配置启动和打包命令
const path = require('path')

module.exports = {
  //webpack配置
  webpack: {
    //配置别名
    alias: {
      //将@符号设置为src目录
      '@': path.resolve(__dirname, 'src'),
    },
  },
}

---路径联想配置(vscode),vscode在输入@/时,会自动联想出来对应的src下的子目录(jsconfig.json)

需要我们在项目下添加jsconfig.json文件,加入配置之后vscode会自动读取配置帮助我们自动联想

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
3.1.3数据Mock

前后端分类的开发模式下,前端可以在没有实际后端接口的支持下进行接口数据的模拟,进行正常业务的功能开发

常见的Mock模式

json-server实现数据Mock

什么是json-server?

json-server是一个node包,可以在不到30秒内获取零编码的完整Mock服务

实现步骤:

1.环境: npm i -D json-server

2.创建一个json文件

文件结构如下:

3.添加启动命令 npm run server新建一个bash窗口再进行启动窗口(下面是如果不想在两个窗口上命令,可以采取的方法)

4.访问接口进行测试

3.1.4.路由

1.创建目录结构

2.实现router相关配置

import Layout from '@/pages/Layout'
import Month from '@/pages/Month'
import New from '@/pages/New'
import Year from '@/pages/Year'
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout></Layout>,
    children: [
      {
        path: 'month',
        element: <Month></Month>,
      },
      {
        path: 'year',
        element: <Year></Year>,
      },
    ],
  },
  {
    path: '/new',
    element: <New></New>,
  },
])
export default route

实现二级路由出口:

import { Outlet } from 'react-router-dom'
const Layout = () => {
  return (
    <div>
      Layout
      <Outlet></Outlet>
    </div>
  )
}
export default Layout

创建全局路由出口:

import { RouterProvider } from 'react-router-dom'
import router from './router'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<RouterProvider router={router}></RouterProvider>)
3.1.5使用redux管理账目列表

创建目录

在全局index中进行使用

import { Provider } from 'react-redux'
import store from './store'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <RouterProvider router={router}></RouterProvider>
  </Provider>
)
3.2重点实现模块:
3.2.1月总结菜单:
import { NavBar, DatePicker } from 'antd-mobile'
import { useState, useEffect } from 'react'
import './index.scss'
import classNames from 'classnames'
import dayjs from 'dayjs'
import { useSelector } from 'react-redux'
import { useMemo } from 'react'
import _groupBy from 'lodash/groupBy'
import DailyBill from './components/DayBill'

const Month = () => {
  const [dateVisible, setDateVisible] = useState(false)
  // 控制显示时间
  const [currentDate, setCurrentDate] = useState(() => {
    return dayjs(new Date()).format('YYYY-MM')
  })
  // 设置当前月份的数据
  const [currentMonthList, setCurrentMonthList] = useState([])
  // 获取账单数据
  const billList = useSelector((state) => state.bill.billList)

  //按月进行数据分组
  const monthGroup = useMemo(() => {
    return _groupBy(billList, (item) => {
      return dayjs(item.date).format('YYYY-MM')
    })
  }, [billList])

  // 当 monthGroup 更新时,设置当前月份的数据,
  useEffect(() => {
    const currentMonth = dayjs(new Date()).format('YYYY-MM')
    setCurrentMonthList(monthGroup[currentMonth] || [])
  }, [monthGroup])

  const onConfirm = (date) => {
    setDateVisible(false)
    const formatDate = dayjs(date).format('YYYY-MM')
    setCurrentMonthList(monthGroup[formatDate] || [])
    setCurrentDate(formatDate)
  }

  // 计算出这个月的总钱数
  const monthResult = useMemo(() => {
    const pay = currentMonthList
      .filter((item) => item.type === 'pay')
      .reduce((acc, item) => {
        return acc + Math.abs(item.money) // 支出取绝对值
      }, 0)
    const income = currentMonthList
      .filter((item) => item.type === 'income')
      .reduce((acc, item) => {
        return acc + item.money // 收入直接相加
      }, 0)
    return {
      pay,
      income,
      total: income - pay, // 结余 = 收入 - 支出
    }
  }, [currentMonthList])

  // 当前月按照日进行分组
  const dayGroup = useMemo(() => {
    const groupData = _groupBy(currentMonthList, (item) =>
      dayjs(item.date).format('YYYY-MM-DD')
    )
    const keys = Object.keys(groupData)
    return {
      groupData,
      keys,
    }
  }, [currentMonthList])
  console.log(dayGroup)

  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setDateVisible(true)}>
            <span className="text">{currentDate + ''} 月账单</span>
            <span
              className={classNames('arrow', dateVisible && 'expand')}
            ></span>
          </div>
          {/* 统计区域 */}
          <div className="twoLineOverview">
            <div className="item">
              <span className="money">{monthResult.pay.toFixed(2)}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.income.toFixed(2)}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.total.toFixed(2)}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={dateVisible}
            onCancel={() => setDateVisible(false)}
            onConfirm={onConfirm}
            onClose={() => setDateVisible(false)}
            max={new Date()}
          />
        </div>
        {dayGroup.keys.map((key) => {
          return (
            <DailyBill
              date={key}
              key={key}
              billList={dayGroup.groupData[key]}
            ></DailyBill>
          )
        })}
      </div>
    </div>
  )
}

export default Month

账单详情组件:

import classNames from 'classnames'
import './index.scss'
import { useMemo } from 'react'
import { billTypeToName } from '@/contants'
import Icon from '@/components/Icon'
import { useState } from 'react'
const DailyBill = ({ date, billList }) => {
  const [visible, setVisible] = useState(false)
  // 计算出这个月的总钱数
  const dayResult = useMemo(() => {
    const pay = billList
      .filter((item) => item.type === 'pay')
      .reduce((acc, item) => {
        return acc + Math.abs(item.money) // 支出取绝对值
      }, 0)
    const income = billList
      .filter((item) => item.type === 'income')
      .reduce((acc, item) => {
        return acc + item.money // 收入直接相加
      }, 0)
    return {
      pay,
      income,
      total: income - pay, // 结余 = 收入 - 支出
    }
  }, [billList])

  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</span>
          <span
            className={classNames('arrow', visible && 'expand')}
            onClick={() => setVisible(!visible)}
          ></span>
        </div>
        <div className="oneLineOverview">
          <div className="pay">
            <span className="type">支出</span>
            <span className="money">{dayResult.pay.toFixed(2)}</span>
          </div>
          <div className="income">
            <span className="type">收入</span>
            <span className="money">{dayResult.income.toFixed(2)}</span>
          </div>
          <div className="balance">
            <span className="money">{dayResult.total.toFixed(2)}</span>
            <span className="type">结余</span>
          </div>
        </div>
      </div>
      {/* 单日列表 */}
      <div className="billList" style={{ display: visible ? 'block' : 'none' }}>
        {billList.map((item) => {
          return (
            <div className="bill" key={item.id}>
              <Icon type={item.useFor}></Icon>
              <div className="detail">
                <div className="billType">{billTypeToName[item.useFor]}</div>
              </div>
              <div className={classNames('money', item.type)}>
                {item.money.toFixed(2)}
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

export default DailyBill

 

3.2.2记账模块:
import { Button, DatePicker, Input, NavBar } from 'antd-mobile'
import Icon from '@/components/Icon'
import './index.scss'
import classNames from 'classnames'
import { billListData } from '@/contants'
import { data, useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { addBillList } from '@/store/modules/billStore'
import { useDispatch } from 'react-redux'
import dayjs from 'dayjs'
const New = () => {
  const navigate = useNavigate()
  const [billType, setBillType] = useState('pay')
  const [money, setMoney] = useState(0)
  const [useFor, setUseFor] = useState('')
  const [dateVisible, setDateVisible] = useState(false)
  const moneyChange = (value) => {
    setMoney(value)
  }
  const dispatch = useDispatch()
  const [date, setDate] = useState()
  const onConfirm = (date) => {
    setDate(date)
    setDateVisible(false)
  }
  const saveBill = () => {
    const data = {
      type: billType,
      money: billType === 'pay' ? -money : +money,
      date: date,
      useFor: useFor,
    }
    if (data.useFor === '') {
      data.useFor = 'food'
    }
    if (money !== 0) {
      dispatch(addBillList(data))
    }
  }

  return (
    <div className="keepAccounts">
      <NavBar className="nav" onBack={() => navigate(-1)}>
        记一笔
      </NavBar>

      <div className="header">
        <div className="kaType">
          <Button
            shape="rounded"
            className={classNames(billType === 'pay' ? 'selected' : '')}
            onClick={() => setBillType('pay')}
          >
            支出
          </Button>
          <Button
            className={classNames(billType === 'income' ? 'selected' : '')}
            shape="rounded"
            onClick={() => setBillType('income')}
          >
            收入
          </Button>
        </div>

        <div className="kaFormWrapper">
          <div className="kaForm">
            <div className="date">
              <Icon type="calendar" className="icon" />
              <span className="text" onClick={() => setDateVisible(true)}>
                {dayjs(date).format('YYYY-MM-DD')}
              </span>
              <DatePicker
                className="kaDate"
                title="记账日期"
                max={new Date()}
                visible={dateVisible}
                onConfirm={onConfirm}
                onClose={() => setDateVisible(false)}
                onCancel={() => setDateVisible(false)}
              />
            </div>
            <div className="kaInput">
              <Input
                className="input"
                placeholder="0.00"
                type="number"
                value={money}
                onChange={moneyChange}
              />
              <span className="iconYuan">¥</span>
            </div>
          </div>
        </div>
      </div>

      <div className="kaTypeList">
        {billListData[billType].map((item) => {
          return (
            <div className="kaType" key={item.type}>
              <div className="title">{item.name}</div>
              <div className="list">
                {item.list.map((item) => {
                  return (
                    <div
                      className={classNames(
                        'item',
                        useFor === item.type ? 'selected' : ''
                      )}
                      key={item.type}
                      onClick={() => setUseFor(item.type)}
                    >
                      <div className="icon">
                        <Icon type={item.type} />
                      </div>
                      <div className="text">{item.name}</div>
                    </div>
                  )
                })}
              </div>
            </div>
          )
        })}
      </div>

      <div className="btns">
        <Button className="btn save" onClick={saveBill}>
          保 存
        </Button>
      </div>
    </div>
  )
}

export default New

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐