React案例实操(三)

月度账单-统计区域

Month.js静态搭建

复制代码
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'

const Month = () => {
  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date">
            <span className="text">
              2023 | 3月账单
            </span>
            <span className='arrow expand'></span>
          </div>
          {/* 统计区域 */}
          <div className='twoLineOverview'>
            <div className="item">
              <span className="money">{100}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={false}
            max={new Date()}
          />
        </div>
      </div>
    </div >
  )
}

export default Month

index.scss

复制代码
.monthlyBill {
  --ka-text-color: #191d26;
  height: 100%;
  background: linear-gradient(180deg, #ffffff, #f5f5f5 100%);
  background-size: 100% 240px;
  background-repeat: no-repeat;
  background-color: rgba(245, 245, 245, 0.9);
  color: var(--ka-text-color);

  .nav {
    --adm-font-size-10: 16px;
    color: #121826;
    background-color: transparent;
    .adm-nav-bar-back-arrow {
      font-size: 20px;
    }
  }

  .content {
    height: 573px;
    padding: 0 10px;
    overflow-y: scroll;
    -ms-overflow-style: none; /* Internet Explorer 10+ */
    scrollbar-width: none; /* Firefox */
    &::-webkit-scrollbar {
      display: none; /* Safari and Chrome */
    }

    > .header {
      height: 135px;
      padding: 20px 20px 0px 18.5px;
      margin-bottom: 10px;
      background-image: url(https://img1.baidu.com/it/u=2338180606,154682688&fm=253&app=138&f=JPEG?w=500&h=889);
      background-size: 100% 100%;

      .date {
        display: flex;
        align-items: center;
        margin-bottom: 25px;
        font-size: 16px;

        .arrow {
          display: inline-block;
          width: 7px;
          height: 7px;
          margin-top: -3px;
          margin-left: 9px;
          border-top: 2px solid #121826;
          border-left: 2px solid #121826;
          transform: rotate(225deg);
          transform-origin: center;
          transition: all 0.3s;
        }
        .arrow.expand {
          transform: translate(0, 2px) rotate(45deg);
        }
      }
    }
  }
  .twoLineOverview {
    display: flex;
    justify-content: space-between;
    width: 250px;
    margin: 0 auto;

    .item {
      display: flex;
      flex-direction: column;

      .money {
        height: 24px;
        line-height: 24px;
        margin-bottom: 5px;
        font-size: 18px;
      }
      .type {
        height: 14px;
        line-height: 14px;
        font-size: 12px;
      }
    }
  }
}

月度账单-统计区域-点击切换时间选择框

点击打开弹框

复制代码
import { useState } from 'react'

const [showDateVisible, setShowDateVisible] = useState(false)
<div className="date" onClick={() => setShowDateVisible(true)}>

点击取消/确认按钮以及蒙层区域都可以关闭弹框

复制代码
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'
import { useState } from 'react'
const Month = () => {
  const [showDateVisible, setShowDateVisible] = useState(false)
  const onConfirm = () => {
    setShowDateVisible(false)
    //触发逻辑
  }
  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setShowDateVisible(true)}>
            <span className="text">
              2023 | 3月账单
            </span>
            <span className='arrow expand'></span>
          </div>
          {/* 统计区域 */}
          <div className='twoLineOverview'>
            <div className="item">
              <span className="money">{100}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={showDateVisible}
            onCancel={() => setShowDateVisible(false)}
            onClose={() => setShowDateVisible(false)}
            onConfirm={onConfirm}
            max={new Date()}

          />
        </div>
      </div>
    </div >
  )
}

export default Month

弹框关闭时箭头朝下,打开是箭头朝上

复制代码
import classNames from 'classnames'
<span className={classNames('arrow',showDateVisible && 'expand')} ></span>

全部代码

复制代码
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'
import { useState } from 'react'
import classNames from 'classnames'
const Month = () => {
  const [showDateVisible, setShowDateVisible] = useState(false)
  const onConfirm = () => {
    setShowDateVisible(false)
    //触发逻辑
  }
  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setShowDateVisible(true)}>
            <span className="text">
              2023 | 3月账单
            </span>
            <span className={classNames('arrow',showDateVisible && 'expand')} ></span>
          </div>
          {/* 统计区域 */}
          <div className='twoLineOverview'>
            <div className="item">
              <span className="money">{100}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={showDateVisible}
            onCancel={() => setShowDateVisible(false)}
            onClose={() => setShowDateVisible(false)}
            onConfirm={onConfirm}
            max={new Date()}

          />
        </div>
      </div>
    </div >
  )
}

export default Month

月度账单-统计区域-点击确定切换时间显示

引入dayjs换日期格式并且写时间控制显示,渲染出来

复制代码
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'
import { useState } from 'react'
import classNames from 'classnames'
import dayjs from 'dayjs'
const Month = () => {
  //日期选择器显示控制
  const [showDateVisible, setShowDateVisible] = useState(false)
  //控制时间显示
  const [date, setDate] = useState(() => {
    return  dayjs(new Date()).format('YYYY-MM')
  })
  const onConfirm = (date) => {
    setShowDateVisible(false)
    //触发逻辑
    const selectDate = dayjs(date).format('YYYY-MM')
    setDate(selectDate)
  }
  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setShowDateVisible(true)}>
            <span className="text">
                {date+''}
            </span>
            <span className={classNames('arrow',showDateVisible && 'expand')} ></span>
          </div>
          {/* 统计区域 */}
          <div className='twoLineOverview'>
            <div className="item">
              <span className="money">{100}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{200}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={showDateVisible}
            onCancel={() => setShowDateVisible(false)}
            onClose={() => setShowDateVisible(false)}
            onConfirm={onConfirm}
            max={new Date()}

          />
        </div>
      </div>
    </div >
  )
}

export default Month

月度账单-统计区域-数据按月分组实现

从Redux拿到数据放入Memo

复制代码
import { useSelector } from 'react-redux'
import { useMemo } from 'react'
//按月份分组
  const billList = useSelector(state=>state.billStore.billList)
  const monthGroup = useMemo(()=>{
    return billList
  },[billList])
  console.log(monthGroup);

按月份分组

复制代码
npm i lodash

应用

复制代码
import _ from 'lodash'
 //按月份分组
  const billList = useSelector(state=>state.billStore.billList)
  const monthGroup = useMemo(()=>{
    return _.groupBy(billList,(item=>dayjs(item.date).format('YYYY-MM')))
  },[billList])
  console.log(monthGroup);

月度账单-统计区域-计算选择月份之后的统计数据

获取当前月并且找到对应数组

复制代码
const [monthList , setMonthList] = useState([])
const onConfirm = (date) => {
    setShowDateVisible(false)
    //触发逻辑
    const selectDate = dayjs(date).format('YYYY-MM')
    //存月份列表
    setMonthList(monthGroup[selectDate])
    setDate(selectDate)
  }

计算

复制代码
  const monthResult = useMemo(()=>{
    //支出和收入结余
    const pay = monthList.filter(item=>item.type==='pay').reduce((a,c)=>a+=c.money,0)
    const income = monthList.filter(item=>item.type==='income').reduce((a,c)=>a+=c.money,0)
    return {
      pay,
      income,
      total:income+pay
    }
  },[monthList])

全部代码

复制代码
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'
import { useState } from 'react'
import classNames from 'classnames'
import dayjs from 'dayjs'
import { useSelector } from 'react-redux'
import { useMemo } from 'react'
import _ from 'lodash'
const Month = () => {
  //按月份分组
  const billList = useSelector(state=>state.billStore.billList)
  const monthGroup = useMemo(()=>{
    return _.groupBy(billList,(item=>dayjs(item.date).format('YYYY-MM')))
  },[billList])
  console.log(monthGroup);
  //存月份
  const [monthList , setMonthList] = useState([])
  const monthResult = useMemo(()=>{
    //支出和收入结余
    const pay = monthList.filter(item=>item.type==='pay').reduce((a,c)=>a+=c.money,0)
    const income = monthList.filter(item=>item.type==='income').reduce((a,c)=>a+=c.money,0)
    return {
      pay,
      income,
      total:income+pay
    }
  },[monthList])
  //日期选择器显示控制
  const [showDateVisible, setShowDateVisible] = useState(false)
  //控制时间显示
  const [date, setDate] = useState(() => {
    return  dayjs(new Date()).format('YYYY-MM')
  })
  const onConfirm = (date) => {
    setShowDateVisible(false)
    //触发逻辑
    const selectDate = dayjs(date).format('YYYY-MM')
    //存月份列表
    setMonthList(monthGroup[selectDate])
    setDate(selectDate)
  }
  return (
    <div className="monthlyBill">
      <NavBar className="nav" backArrow={false}>
        月度收支
      </NavBar>
      <div className="content">
        <div className="header">
          {/* 时间切换区域 */}
          <div className="date" onClick={() => setShowDateVisible(true)}>
            <span className="text">
                {date+''}
            </span>
            <span className={classNames('arrow',showDateVisible && 'expand')} ></span>
          </div>
          {/* 统计区域 */}
          <div className='twoLineOverview'>
            <div className="item">
              <span className="money">{monthResult.pay}</span>
              <span className="type">支出</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.income}</span>
              <span className="type">收入</span>
            </div>
            <div className="item">
              <span className="money">{monthResult.total}</span>
              <span className="type">结余</span>
            </div>
          </div>
          {/* 时间选择器 */}
          <DatePicker
            className="kaDate"
            title="记账日期"
            precision="month"
            visible={showDateVisible}
            onCancel={() => setShowDateVisible(false)}
            onClose={() => setShowDateVisible(false)}
            onConfirm={onConfirm}
            max={new Date()}

          />
        </div>
      </div>
    </div >
  )
}

export default Month

月度账单-统计区域-初始化渲染统计数据

重构一下数据

复制代码
{
   "ka":[
     {
        "type": "pay",
        "money": -100,
        "date": "2022-01-01 10:00:00",
        "useFor": "drinks",
        "id": 1
    },
    {
        "type": "pay",
        "money": 80,
        "date": "2022-01-01 10:30:00",
        "useFor": "iong",
        "id": 1
    },
    {
        "type": "pay",
        "money": -9,
        "date": "2022-01-31 8:00:00",
        "useFor": "desserts",
        "id": 1
    },
    {
        "type": "income",
        "money": -10,
        "date": "2022-01-31 8:30:00",
        "useFor": "drinks",
        "id": 1
    }, {
        "type": "pay",
        "money": -1900,
        "date": "2026-01-18 8:00:00",
        "useFor": "desserts",
        "id": 1
    },
    {
        "type": "income",
        "money": -10,
        "date": "2026-01-19 8:30:00",
        "useFor": "drinks",
        "id": 1
    },
     {
        "type": "income",
        "money": -10,
        "date": "2026-01-20 8:30:00",
        "useFor": "drinks",
        "id": 1
    }
   ]
}
复制代码
//初始化把当前月显示出来
  useEffect(()=>{
    const now = dayjs().format('YYYY-MM')
    //边界值控制
    if(monthGroup[now]){
      setMonthList(monthGroup[now])
    }
  },[monthGroup])

月度账单-列表区域-单日统计列表实现

新建组件文件

静态搭建

复制代码
import classNames from 'classnames'
import './index.scss'

const DailyBill = () => {
  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{'03月23日'}</span>
          <span className={classNames('arrow')}></span>
        </div>
        <div className="oneLineOverview">
          <div className="pay">
            <span className="type">支出</span>
            <span className="money">{100}</span>
          </div>
          <div className="income">
            <span className="type">收入</span>
            <span className="money">{200}</span>
          </div>
          <div className="balance">
            <span className="money">{100}</span>
            <span className="type">结余</span>
          </div>
        </div>
      </div>
    </div>
  )
}
export default DailyBill

index.scss

复制代码
.dailyBill {
  margin-bottom: 10px;
  border-radius: 10px;
  background: #ffffff;

  .header {
    --ka-text-color: #888c98;
    padding: 15px 15px 10px 15px;

    .dateIcon {
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 21px;
      margin-bottom: 9px;
      .arrow {
        display: inline-block;
        width: 5px;
        height: 5px;
        margin-top: -3px;
        margin-left: 9px;
        border-top: 2px solid #888c98;
        border-left: 2px solid #888c98;
        transform: rotate(225deg);
        transform-origin: center;
        transition: all 0.3s;
      }
      .arrow.expand {
        transform: translate(0, 2px) rotate(45deg);
      }

      .date {
        font-size: 14px;
      }
    }
  }
  .oneLineOverview {
    display: flex;
    justify-content: space-between;

    .pay {
      flex: 1;
      .type {
        font-size: 10px;
        margin-right: 2.5px;
        color: #e56a77;
      }
      .money {
        color: var(--ka-text-color);
        font-size: 13px;
      }
    }

    .income {
      flex: 1;
      .type {
        font-size: 10px;
        margin-right: 2.5px;
        color: #4f827c;
      }
      .money {
        color: var(--ka-text-color);
        font-size: 13px;
      }
    }

    .balance {
      flex: 1;
      margin-bottom: 5px;
      text-align: right;

      .money {
        line-height: 17px;
        margin-right: 6px;
        font-size: 17px;
      }
      .type {
        font-size: 10px;
        color: var(--ka-text-color);
      }
    }
  }

  .billList {
    padding: 15px 10px 15px 15px;
    border-top: 1px solid #ececec;
    .bill {
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 43px;
      margin-bottom: 15px;

      &:last-child {
        margin-bottom: 0;
      }

      .icon {
        margin-right: 10px;
        font-size: 25px;
      }
      .detail {
        flex: 1;
        padding: 4px 0;
        .billType {
          display: flex;
          align-items: center;
          height: 17px;
          line-height: 17px;
          font-size: 14px;
          padding-left: 4px;
        }
      }
      .money {
        font-size: 17px;

        &.pay {
          color: #ff917b;
        }
        &.income {
          color: #4f827c;
        }
      }
    }
  }
}
.dailyBill.expand {
  .header {
    border-bottom: 1px solid #ececec;
  }
  .billList {
    display: block;
  }
}

引入

复制代码
import DailyBill from './commponents/DayBill'
{/* 每日账本 */}
        <DailyBill />
      </div>
    </div >

把当前月的数据按照日来分组

month.js

复制代码
//按月份分组
  const billList = useSelector(state=>state.billStore.billList)
  const monthGroup = useMemo(()=>{
    return _.groupBy(billList,(item=>dayjs(item.date).format('YYYY-MM')))
  },[billList])
  console.log(monthGroup);

  //初始化把当前月显示出来
  useEffect(()=>{
    const now = dayjs().format('YYYY-MM')
    //边界值控制
    if(monthGroup[now]){
      setMonthList(monthGroup[now])
    }
  },[monthGroup])
  //存月份
  const [monthList , setMonthList] = useState([])
  const monthResult = useMemo(()=>{
    //支出和收入结余
    const pay = monthList.filter(item=>item.type==='pay').reduce((a,c)=>a+=c.money,0)
    const income = monthList.filter(item=>item.type==='income').reduce((a,c)=>a+=c.money,0)
    return {
      pay,
      income,
      total:income+pay
    }
  },[monthList])
    //按日分组
  // 把当前月按日分组账单数据
  const dayGroup = useMemo(() => {
    const group = _.groupBy(monthList, (item) => dayjs(item.date).format('YYYY-MM-DD'))
    return {
      dayKeys: Object.keys(group),
      group
    }
  }, [monthList])
  console.log(dayGroup)
  {/* 单日列表统计 */}
        {/* 日账单 */}
        {dayGroup.dayKeys.map(dayKey => (
          <DailyBill key={dayKey} date={dayKey} billList={dayGroup.group[dayKey]} />
        ))}

      </div>
    </div >

接收数据计算统计渲染页面

复制代码
import classNames from 'classnames'
import './index.scss'
import { useMemo } from 'react'

const DailyBill = ({ date, billList }) => {
  const dayResult = useMemo(() => {
    // 支出  /  收入  / 结余
    const pay = billList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)
    const income = billList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)
    return {
      pay,
      income,
      total: pay + income
    }
  }, [billList])
  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</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>
  )
}

export default DailyBill

月度账单-列表区域-单日账单列表渲染显示

在子组件静态列表

复制代码
import classNames from 'classnames'
import './index.scss'
import { useMemo } from 'react'

const DailyBill = ({ date, billList }) => {
  const dayResult = useMemo(() => {
    // 支出  /  收入  / 结余
    const pay = billList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)
    const income = billList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)
    return {
      pay,
      income,
      total: pay + income
    }
  }, [billList])
  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</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">
        {billList.map(item => {
            return (
            <div className="bill" key={item.id}>
                <div className="detail">
                <div className="billType">{item.useFor}</div>
                </div>
                <div className={classNames('money', item.type)}>
                {item.money.toFixed(2)}
                </div>
            </div>
            )
        })}
        </div>
    </div>
  )
}

export default DailyBill

准备静态数据

复制代码
export const billListData = {
  pay: [
    {
      type: 'foods',
      name: '餐饮',
      list: [
        { type: 'food', name: '餐费' },
        { type: 'drinks', name: '酒水饮料' },
        { type: 'dessert', name: '甜品零食' },
      ],
    },
    {
      type: 'taxi',
      name: '出行交通',
      list: [
        { type: 'taxi', name: '打车租车' },
        { type: 'longdistance', name: '旅行票费' },
      ],
    },
    {
      type: 'recreation',
      name: '休闲娱乐',
      list: [
        { type: 'bodybuilding', name: '运动健身' },
        { type: 'game', name: '休闲玩乐' },
        { type: 'audio', name: '媒体影音' },
        { type: 'travel', name: '旅游度假' },
      ],
    },
    {
      type: 'daily',
      name: '日常支出',
      list: [
        { type: 'clothes', name: '衣服裤子' },
        { type: 'bag', name: '鞋帽包包' },
        { type: 'book', name: '知识学习' },
        { type: 'promote', name: '能力提升' },
        { type: 'home', name: '家装布置' },
      ],
    },
    {
      type: 'other',
      name: '其他支出',
      list: [{ type: 'community', name: '社区缴费' }],
    },
  ],
  income: [
    {
      type: 'professional',
      name: '其他支出',
      list: [
        { type: 'salary', name: '工资' },
        { type: 'overtimepay', name: '加班' },
        { type: 'bonus', name: '奖金' },
      ],
    },
    {
      type: 'other',
      name: '其他收入',
      list: [
        { type: 'financial', name: '理财收入' },
        { type: 'cashgift', name: '礼金收入' },
      ],
    },
  ],
}

export const billTypeToName = Object.keys(billListData).reduce((prev, key) => {
  billListData[key].forEach(bill => {
    bill.list.forEach(item => {
      prev[item.type] = item.name
    })
  })
  return prev
}, {})
复制代码
import { billTypeToName } from '@/pages/contants/index'
<div className="billType">{billTypeToName[item.useFor]}</div>

全部代码

复制代码
import classNames from 'classnames'
import './index.scss'
import { useMemo } from 'react'
import { billTypeToName } from '@/pages/contants/index'
const DailyBill = ({ date, billList }) => {
  const dayResult = useMemo(() => {
    // 支出  /  收入  / 结余
    const pay = billList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)
    const income = billList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)
    return {
      pay,
      income,
      total: pay + income
    }
  }, [billList])
  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</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">
        {billList.map(item => {
            return (
            <div className="bill" key={item.id}>
                <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

月度账单-切换打开关闭

控制显示隐藏

复制代码
// 声明状态
const [visible, setVisible] = useState(false)

// 控制箭头
 <span 
   className={classNames('arrow', !visible && 'expand')} 
   onClick={() => setVisible(!visible)}></span>
     
// 控制列表显示
<div className="billList" style={{ display: !visible && 'none' }}></div>

全部代码

复制代码
import classNames from 'classnames'
import './index.scss'
import { useMemo } from 'react'
import { billTypeToName } from '@/pages/contants/index'
import { useState } from 'react'
const DailyBill = ({ date, billList }) => {
  const dayResult = useMemo(() => {
    // 支出  /  收入  / 结余
    const pay = billList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)
    const income = billList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)
    return {
      pay,
      income,
      total: pay + income
    }
  }, [billList])
  //控制显示
  const [visable,setVisable]=useState(false)
  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</span>
            <span className={classNames('arrow', visable && 'expand')} onClick={()=>setVisable(!visable)}></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:visable?'block':'none'}}>
        {billList.map(item => {
            return (
            <div className="bill" key={item.id}>
                <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

月度账单-账单类型图标组件封装

新建图标组件

复制代码
const Icon = ({type}) => {
  return (
    <img
      src={`https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/ka/${type}.svg`}
      alt="icon"
      style={{
        width: 20,
        height: 20,
      }}
      />
  )
}

export default Icon

引用图标组件

复制代码
import Icon from '@/components/icon'
  {/* 单日列表 */}
        <div className="billList" style={{display:visable?'block':'none'}}>
        {billList.map(item => {
            return (
            <div className="bill" key={item.id}>
                 <Icon type={item.useFor} />
                <div className="detail">
                <div className="billType">{billTypeToName[item.useFor]}</div>
                </div>
                <div className={classNames('money', item.type)}>
                {item.money.toFixed(2)}
                </div>
            </div>
            )
        })}
        </div>

全部代码

复制代码
import classNames from 'classnames'
import './index.scss'
import { useMemo } from 'react'
import { billTypeToName } from '@/pages/contants/index'
import { useState } from 'react'
import Icon from '@/components/icon'
const DailyBill = ({ date, billList }) => {
  const dayResult = useMemo(() => {
    // 支出  /  收入  / 结余
    const pay = billList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)
    const income = billList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)
    return {
      pay,
      income,
      total: pay + income
    }
  }, [billList])
  //控制显示
  const [visable,setVisable]=useState(false)
  return (
    <div className={classNames('dailyBill')}>
      <div className="header">
        <div className="dateIcon">
          <span className="date">{date}</span>
            <span className={classNames('arrow', visable && 'expand')} onClick={()=>setVisable(!visable)}></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:visable?'block':'none'}}>
        {billList.map(item => {
            return (
            <div className="bill" key={item.id}>
                 <Icon type={item.useFor} />
                <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

新增账单-结构搭建

静态搭建

复制代码
import { Button, DatePicker, Input, NavBar } from 'antd-mobile'
import Icon from '@/components/icon/index.js'
import './index.scss'
import classNames from 'classnames'
import { billListData } from '@/pages/contants/index'
import { useNavigate } from 'react-router-dom'

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

      <div className="header">
        <div className="kaType">
          <Button
            shape="rounded"
            className={classNames('selected')}
          >
            支出
          </Button>
          <Button
            className={classNames('')}
            shape="rounded"
          >
            收入
          </Button>
        </div>

        <div className="kaFormWrapper">
          <div className="kaForm">
            <div className="date">
              <Icon type="calendar" className="icon" />
              <span className="text">{'今天'}</span>
              <DatePicker
                className="kaDate"
                title="记账日期"
                max={new Date()}
              />
            </div>
            <div className="kaInput">
              <Input
                className="input"
                placeholder="0.00"
                type="number"
              />
              <span className="iconYuan">¥</span>
            </div>
          </div>
        </div>
      </div>

      <div className="kaTypeList">
        {billListData['pay'].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',
                        ''
                      )}
                      key={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">
          保 存
        </Button>
      </div>
    </div>
  )
}

export default New

index.scss

复制代码
.keepAccounts {
  --ka-bg-color: #daf2e1;
  --ka-color: #69ae78;
  --ka-border-color: #191d26;

  height: 100%;
  background-color: var(--ka-bg-color);

  .nav {
    --adm-font-size-10: 16px;
    color: #121826;
    background-color: transparent;
    &::after {
      height: 0;
    }

    .adm-nav-bar-back-arrow {
      font-size: 20px;
    }
  }

  .header {
    height: 132px;

    .kaType {
      padding: 9px 0;
      text-align: center;

      .adm-button {
        --adm-font-size-9: 13px;

        &:first-child {
          margin-right: 10px;
        }
      }
      .selected {
        color: #fff;
        --background-color: var(--ka-border-color);
      }
    }

    .kaFormWrapper {
      padding: 10px 22.5px 20px;

      .kaForm {
        display: flex;
        padding: 11px 15px 11px 12px;
        border: 0.5px solid var(--ka-border-color);
        border-radius: 9px;
        background-color: #fff;

        .date {
          display: flex;
          align-items: center;
          height: 28px;
          padding: 5.5px 5px;
          border-radius: 4px;
          // color: #4f825e;
          color: var(--ka-color);
          background-color: var(--ka-bg-color);

          .icon {
            margin-right: 6px;
            font-size: 17px;
          }
          .text {
            font-size: 16px;
          }
        }

        .kaInput {
          flex: 1;
          display: flex;
          align-items: center;

          .input {
            flex: 1;
            margin-right: 10px;
            --text-align: right;
            --font-size: 24px;
            --color: var(--ka-color);
            --placeholder-color: #d1d1d1;
          }

          .iconYuan {
            font-size: 24px;
          }
        }
      }
    }
  }

  .container {
  }
  .kaTypeList {
    height: 490px;
    padding: 20px 11px;
    padding-bottom: 70px;
    overflow-y: scroll;
    background: #ffffff;
    border-radius: 20px 20px 0 0;
    -ms-overflow-style: none; /* Internet Explorer 10+ */
    scrollbar-width: none; /* Firefox */
    &::-webkit-scrollbar {
      display: none; /* Safari and Chrome */
    }

    .kaType {
      margin-bottom: 25px;
      font-size: 12px;
      color: #333;

      .title {
        padding-left: 5px;
        margin-bottom: 5px;
        font-size: 13px;
        color: #808080;
      }
      .list {
        display: flex;

        .item {
          width: 65px;
          height: 65px;
          padding: 9px 0;
          margin-right: 7px;
          text-align: center;
          border: 0.5px solid #fff;
          &:last-child {
            margin-right: 0;
          }

          .icon {
            height: 25px;
            line-height: 25px;
            margin-bottom: 5px;
            font-size: 25px;
          }
        }
        .item.selected {
          border: 0.5px solid var(--ka-border-color);
          border-radius: 5px;
          background: var(--ka-bg-color);
        }
      }
    }
  }

  .btns {
    position: fixed;
    bottom: 15px;
    width: 100%;
    text-align: center;

    .btn {
    width: 200px;
    --border-width: 0;
    --background-color: #fafafa;
    --text-color: #616161;
    &:first-child {
    margin-right: 15px;
    }
    }
    .btn.save {
    --background-color: var(--ka-bg-color);
    --text-color: var(--ka-color);
    }
    }
  }

新增账单-支出和收入功能实现

复制代码
const new = ()=>{
  // 1. 区分账单状态
  const [billType, setBillType] = useState('income')
  return (
     <div className="keepAccounts">
      <div className="kaType">
        {/* 2. 点击切换状态 */}
        <Button
          shape="rounded"
          className={classNames(billType==='pay'?'selected':'')}
          onClick={() => setBillType('pay')}
        >
          支出
        </Button>
        <Button
          className={classNames(billType==='income'?'selected':'')}
          onClick={() => setBillType('income')}
          shape="rounded"
        >
          收入
        </Button>
      </div>
      {/* 2. 适配数据 */}
      <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',
                        ''
                      )}
                      key={item.type}

                    >
                      <div className="icon">
                        <Icon type={item.type} />
                      </div>
                      <div className="text">{item.name}</div>
                    </div>
                  )
          })}
      </div>
    </div>
  )
}

新增账单-新增表单实现

收集金额

复制代码
 const [cunMoney,setcunMoney]=useState(0)
  const ChangeMoney=(value)=>{
    setcunMoney(value)//存储金额
  } 
  //存储账单类型
  const [useFor,setuseFor]=useState('')
   const dispatch = useDispatch();
  //存储金额类型+调用接口
  const MonthData = ()=>{
    const data={
      type:billtype,
      money:cunMoney,
      date:new Date().getTime(),
      useFor:useFor
    }
    console.log(data);
  }
  return (
            <div className="kaInput">
              <Input
                className="input"
                placeholder="0.00"
                type="number"
                value={cunMoney}
                onChange={ChangeMoney}
              />


      <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',
                        ''
                      )}
                      key={item.type}
                      onClick={()=>setuseFor(item.type)}
                    >
                     
      <div className="btns">
        <Button className="btn save" onClick={MonthData}>
          保 存
        </Button>
      </div>
    </div>
  )
}

export default New

Redux存储

复制代码
//账单相关store
import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const billStore = createSlice({
  name: "billStore",
  initialState: {
    billList: [],
  },
  reducers: {
    //同步修改方法
    setBillList(state, action) {
        state.billList = action.payload;
    },
    //同步修改新增账单方法
    addbillList(state, action) {
        state.billList.push(action.payload);
    }
  }
})
const { setBillList,addbillList } = billStore.actions;
//编写异步
const getBillList=()=> {
    return async (dispatch)=>{
        const res = await axios.get("http://localhost:8888/ka");
        dispatch(setBillList(res.data));
    }
}
const getAddBillList=(data)=> {
    return async (dispatch)=>{
        const res = await axios.post("http://localhost:8888/ka",data);
        dispatch(addbillList(res.data));
    }
}
export { getBillList, getAddBillList };
const reducer = billStore.reducer;
export default reducer;

action提交

复制代码
  //存储金额类型+调用接口
  const MonthData = ()=>{
    const data={
      type:billtype,
      money:cunMoney,
      date:new Date().getTime(),
      useFor:useFor

    }
    console.log(data);
    dispatch(getAddBillList(data))
  }

全部代码

复制代码
import { Button, DatePicker, Input, NavBar } from 'antd-mobile'
import Icon from '@/components/icon/index.js'
import './index.scss'
import classNames from 'classnames'
import { billListData } from '@/pages/contants/index'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { getAddBillList } from '@/store/modules/billStore'
import { useDispatch } from 'react-redux'
const New = () => {
  const navigate = useNavigate()
  const [billtype,setbilltype]=useState('income')

  const [cunMoney,setcunMoney]=useState(0)
  const ChangeMoney=(value)=>{
    setcunMoney(value)//存储金额
  } 
  //存储账单类型
  const [useFor,setuseFor]=useState('')
   const dispatch = useDispatch();
  //存储金额类型+调用接口
  const MonthData = ()=>{
    const data={
      type:billtype,
      money:cunMoney,
      date:new Date().getTime(),
      useFor:useFor

    }
    console.log(data);
    dispatch(getAddBillList(data))
  }
  return (
    <div className="keepAccounts">
      <NavBar className="nav" onBack={() => navigate(-1)}>
        记一笔
      </NavBar>

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

        <div className="kaFormWrapper">
          <div className="kaForm">
            <div className="date">
              <Icon type="calendar" className="icon" />
              <span className="text">{'今天'}</span>
              <DatePicker
                className="kaDate"
                title="记账日期"
                max={new Date()}
              />
            </div>
            <div className="kaInput">
              <Input
                className="input"
                placeholder="0.00"
                type="number"
                value={cunMoney}
                onChange={ChangeMoney}
              />
              <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',
                        ''
                      )}
                      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={MonthData}>
          保 存
        </Button>
      </div>
    </div>
  )
}

export default New

整体优化

判断选中样式

复制代码
  <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>

添加时间控制器

复制代码
import { Button, DatePicker, Input, NavBar } from 'antd-mobile'
import Icon from '@/components/icon/index.js'
import './index.scss'
import classNames from 'classnames'
import { billListData } from '@/pages/contants/index'
import { useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { getAddBillList } from '@/store/modules/billStore'
import { useDispatch } from 'react-redux'
import dayjs from 'dayjs'
const New = () => {
  const navigate = useNavigate()
  const [billtype,setbilltype]=useState('income')
  //时间控制器状态
  const [daydate,setdaydate]=useState(false)
  const [cunMoney,setcunMoney]=useState(0)
  //存储选择时间
  const [day,setday]=useState(new Date())
  const dataConfirm  =(date)=>{
    //点击关闭
    setday(date)
    setdaydate(false)
  }
  const ChangeMoney=(value)=>{
    setcunMoney(value)//存储金额
  } 
  //存储账单类型
  const [useFor,setuseFor]=useState('')
   const dispatch = useDispatch();
  //存储金额类型+调用接口
  const MonthData = ()=>{
    const data={
      type:billtype,
      money:cunMoney,
      date:new Date().getTime(),
      useFor:useFor
    }
    console.log(data);
    dispatch(getAddBillList(data))
    
  }
  return (
    <div className="keepAccounts">
      <NavBar className="nav" onBack={() => navigate(-1)}>
        记一笔
      </NavBar>

      <div className="header">
        <div className="kaType">
          <Button
            shape="rounded"
            className={classNames('selected')}
            onClick={()=>setbilltype('pay')}
          >
            支出
          </Button>
          <Button
            className={classNames('')}
            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={()=>setdaydate(true)}>{dayjs(day).format('YYYY-MM-DD')}</span>
              <DatePicker
                className="kaDate"
                title="记账日期"
                max={new Date()}
                visible={daydate}
                onClose={()=>setdaydate(false)}
                onConfirm={dataConfirm}
              />
            </div>
            <div className="kaInput">
              <Input
                className="input"
                placeholder="0.00"
                type="number"
                value={cunMoney}
                onChange={ChangeMoney}
              />
              <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={MonthData}>
          保 存
        </Button>
      </div>
    </div>
  )
}

export default New
相关推荐
棒棒的唐2 小时前
适合小程序使用的将对象数组转换为参数字符串方法
前端·javascript·小程序
博客zhu虎康2 小时前
音频视频处理:前端直播流播放 flv
前端
一位搞嵌入式的 genius2 小时前
深入理解 JavaScript 原型与继承:从基础到进阶
开发语言·前端·javascript
董世昌412 小时前
深度解析var、let、const的区别与最佳使用场景
开发语言·前端·javascript
C_心欲无痕2 小时前
Next.js 平行路由:构建模块化动态布局
开发语言·前端·javascript
warrah2 小时前
前端项目容器化部署问题
前端·docker
GISer_Jing2 小时前
2026前端技术潜在主流前沿方向
前端·人工智能·reactjs
切糕师学AI3 小时前
Vue 中的生命周期钩子
前端·javascript·vue.js
掘金-我是哪吒3 小时前
提升服务器性能,解决前端首页加载过慢的问题
运维·服务器·前端
摘星编程3 小时前
React Native for OpenHarmony 实战:Platform 平台检测与判断
javascript·react native·react.js