【React】ReactRouter记账本案例实现

记账本

1、环境搭建

使用CRA/vite创建项目,并安装必要依赖,包括下列基础包

  1. Redux状态管理 - @reduxjs/toolkit 、 react-redux
  2. 路由 - react-router-dom
  3. 时间处理 - dayjs
  4. class类名处理 - classnames
  5. 移动端组件库 - antd-mobile
  6. 请求插件 - axios

检查

清理文件


初始化git,连接远程gitee仓库

第一次提交:创建项目并初始化


2、配置别名路径

2-1 背景知识

  1. 路径解析配置(webpack),把 @/ 解析为 src/
  2. 路径联想配置(VsCode),VsCode 在输入 @/ 时,自动联想出来对应的 src/下的子级目录

2-2 路径解析配置

配置步骤:(使用CRA创建项目需要进行以下操作)

  1. 安装craco npm i -D @craco/craco
  2. 项目根目录下创建配置文件 craco.config.js
  3. 配置文件中添加路径解析配置
  4. 包文件中配置启动和打包命令

Vite 本身内置了路径别名(别名解析) 的配置能力,无需额外安装类似 craco 的插件,直接在 vite.config.js/ts 中通过 resolve.alias 配置即可实现路径解析,这是 Vite 原生支持的核心特性,比 CRA + craco 更简洁。

vite.config.js 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import * as path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    // 路径别名配置
    alias: {
      // 配置 @ 指向 src 目录
      '@': path.resolve(__dirname, 'src'),
    },
  },
})

2-3 联想路径配置

本质是给项目中冗长的相对路径设置 "简写别名",并让编辑器 / 构建工具能识别这些别名,核心作用是简化路径书写、提升开发效率、增强代码可维护性

配置步骤:(vite和CRA创建都需要)

  1. 根目录下新增配置文件 - jsconfig.json
  2. 添加路径提示配置
json 复制代码
{
  "compilerOptions":{
    "baseUrl":"./",
    "paths":{
      "@/*":[
        "src/*"
      ]
    }
  }
}

完成之后提交一次代码

3、数据Mock实现

3-1 常见的Mock方式

其实就是模拟假数据,在没有实际后端接口的支持下,进行正常业务开发

3-2 json-server实现Mock

json-server是一个node包

实现步骤:

  1. 项目中安装json-server npm i -D json-server
  2. 准备一个json文件 (素材里获取)
  3. 添加启动命令
  4. 访问接口进行测试
server/data.json 复制代码
{
  "ka": [
    {
      "type": "pay",
      "money": -99,
      "date": "2022-10-24 10:36:42",
      "useFor": "drinks",
      "id": 1
    },
    {
      "type": "pay",
      "money": -88,
      "date": "2022-10-24 10:37:51",
      "useFor": "longdistance",
      "id": 2
    },
    {
      "type": "income",
      "money": 100,
      "date": "2022-10-22 00:00:00",
      "useFor": "bonus",
      "id": 3
    },
    {
      "type": "pay",
      "money": -33,
      "date": "2022-09-24 16:15:41",
      "useFor": "dessert",
      "id": 4
    },
    {
      "type": "pay",
      "money": -56,
      "date": "2022-10-22T05:37:06.000Z",
      "useFor": "drinks",
      "id": 5
    },
    {
      "type": "pay",
      "money": -888,
      "date": "2022-10-28T08:21:42.135Z",
      "useFor": "travel",
      "id": 6
    },
    {
      "type": "income",
      "money": 10000,
      "date": "2023-03-20T06:45:54.004Z",
      "useFor": "salary",
      "id": 7
    },
    {
      "type": "pay",
      "money": -10,
      "date": "2023-03-22T07:17:12.531Z",
      "useFor": "drinks",
      "id": 8
    },
    {
      "type": "pay",
      "money": -20,
      "date": "2023-03-22T07:51:20.421Z",
      "useFor": "dessert",
      "id": 9
    },
    {
      "type": "pay",
      "money": -100,
      "date": "2023-03-22T09:18:12.898Z",
      "useFor": "drinks",
      "id": 17
    },
    {
      "type": "pay",
      "money": -50,
      "date": "2023-03-23T09:11:23.312Z",
      "useFor": "food",
      "id": 18
    },
    {
      "type": "pay",
      "money": -10,
      "date": "2023-04-03T11:14:56.036Z",
      "useFor": "food",
      "id": 19
    }
  ]
}

在package.json中添加启动命令,以这个文件夹下的data.json为数据源,开启一个端口号为8888的接口服务

复制到浏览器访问可以得到数据(get请求

完成之后提交一次代码,添加mock服务

4、整体路由设计

  1. 俩个一级路由 (Layout / new)
  2. 俩个二级路由 (Layout - mouth/year)

实践:

注:一级组件Layout中要添加<Outlet />组件,才能在「父路由组件」中渲染「子路由匹配的组件」



创建路由实例,绑定path element

引入组件

5、antD主题定制

Ant-Design-Mobile官方文档

5-1 定制方案

5-2 实现方式

  1. 全局定制
src/theme.css 复制代码
/* 全局定制 */
:root:root {
  --adm-color-primary: rgb(105, 174, 120);
}
  1. 局部定制
src/theme.css 复制代码
/* 局部定制 */
.puple {
  --adm-color-primary: rgb(104, 108, 231);
}

在main.jsx中导入主题文件

在Layout/index.jsx中测试效果


记账本使用全局配置

:root:root {

--adm-color-primary: rgb(105, 174, 120);

}

完成之后提交代码

也可以在终端使用命令提交

复制代码
git add .

git commit - m "完成主题颜色的定制化"

6、Redux管理账目列表

  1. 创建文件
  1. 编写账单相关store
  1. 组合子模块 导出store实例

4.导入store

  1. 触发异步请求

6.测试是否成功

注意:测试之前需要先把server服务打开,然后再把前端项目打开

测试完成之后提交代码

合并启动mock服务和前端服务的命令

复制代码
每次都要先启动mock服务,在启动前端服务比较繁琐,可以将两条命令简化话为一条

Linux/macOS

Linux/macOS:& 表示 "后台并行执行两个命令"

在start/dev中直接修改
"dev": "vite & npm run server"
"start": "craco start & npm run server"

Windows

Windows (cmd/PowerShell):& 会被解析为参数,不能直接写
步骤 1:安装 concurrently(concurrently 依赖 "已定义的独立脚本" 来调用)
npm i -D concurrently

步骤 2:修改 package.json中的scripts

添加1项 "start": "concurrently \"npm run dev\" \"npm run server\""

或者在dev里直接按start的格式添加npm run server也是可以的

直接运行npm run dev

7、TabBar功能实现

需求:使用antD的TabBar标签栏组件进行布局以及路由的切换

实现方式:看文档(找到相似Demo-复制代码跑通-定制化修改)
Tab-Bar组件

7-1 静态布局实现

npm i -D sass

scr/pages/layout/index.jsx 复制代码
import { TabBar } from "antd-mobile"
import { useEffect } from "react"
import { Outlet } from "react-router-dom"
import { useDispatch } from 'react-redux'
import { getBillList } from "@/store/modules/billStore"
import './index.css'
import {
  BillOutline,
  CalculatorOutline,
  AddCircleOutline
} from 'antd-mobile-icons'

const tabs = [
  {
    key: '/month',
    title: '月度账单',
    icon: <BillOutline />,
  },
  {
    key: '/new',
    title: '记账',
    icon: <AddCircleOutline />,
  },
  {
    key: '/year',
    title: '年度账单',
    icon: <CalculatorOutline />,
  },
]

const Layout = () => {
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(getBillList())
  }, [dispatch])
  return (
    <div className="layout">
      <div className="container">
        <Outlet />
      </div>
      <div className="footer">
        <TabBar>
          {tabs.map(item => (
            <TabBar.Item key={item.key} icon={item.icon} title={item.title} />
          ))}
        </TabBar>
      </div>
    </div>
  )
}

export default Layout
scr/pages/layout/index.scss 复制代码
.layout {
  .container {
    position: fixed;
    top: 0;
    bottom: 50px;
    width: 100%;
  }

  .footer {
    position: fixed;
    bottom: 0;
    width: 100%;
  }
}

7-2 切换路由实现

复制代码
// 切换菜单跳转路由
  const navigate = useNavigate()
  const swithRoute = (path) => {
    console.log(path)
    navigate(path)
  }

  return (
    <div className="layout">
      <div className="footer">
        <TabBar onChange={swithRoute}>
          {/* 省略... */}
        </TabBar>
      </div>
    </div>
  )

path 参数是 antd-mobile 的 TabBar 组件的 onChange 回调自动传入的,核心是 TabBar.Itemkey 值会被作为回调参数传递

测试

完成后提交代码

8、月度账单-统计区域

实现以下效果

功能点

  1. 点击切换月份
  2. 适配箭头显示
  3. 统计指出、收入、结余数据

8-1 准备静态结构

scr/pages/Month/index.jsx 复制代码
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
scr/pages/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://zqran.gitee.io/images/ka/month-bg.png);
      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;

    .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;
      }
    }
  }
}

图片地址已经失效了,需要自己替换一图片 或者直接加一个background-color: #e9eda9;,显示更直观

完成后提交代码"统计区域功能熟悉与静态结构搭建"

8-2 点击切换时间选择框

功能要求:

  1. 点击打开时间选择弹框
  2. 点击取消/确认按钮以及蒙层区域都可以关闭弹框
  3. 弹框关闭时箭头朝下,打开是箭头朝上

实现思路:

  1. 准备一个状态数据
  2. 点击切换状态
  3. 根据状态控制弹框打开关闭以及箭头样式

Picker 选择器

属性 说明 类型 默认值
onCancel 取消操作时触发 () => void -
onClose 确认或取消操作时,均触发弹窗关闭事件 () => void -
onConfirm 确认操作时触发 (value: PickerValue[], extend: PickerValueExtend) => void -

提交代码

8-3 切换时间显示

实现思路

  1. 创建一个控制时间显示的状态
  2. 拿到当前选中的时间赋值给状态

    实现效果

    提交代码

8-4 统计功能实现

实现思路:

  1. 按月分组
  2. 根据获取到的时间作为key取当月的账单数组
  3. 根据当月的账单数组计算支出、收入、总计

按月分组得到当月账单数组

复制代码
npm i lodash


lodash-groupBy函数

例子 复制代码
_.groupBy([6.1, 4.2, 6.3], Math.floor);
// => { '4': [4.2], '6': [6.1, 6.3] }
 
// The `_.property` iteratee shorthand.
_.groupBy(['one', 'two', 'three'], 'length');
// => { '3': ['one', 'two'], '5': ['three'] }

key就是分组要求key,value是满足某一个分组要求的所有元素

提交代码

计算选择月份的统计数据

点击时间确认按钮之后,把当前月份的统计数据计算出来显示到页面中

实现思路

  1. 点击确认获取到当前月份
  2. 按月分组数据中找到对应数组
  3. 基于数组做计算,使用useMemo-reduce


月度初始化时渲染统计数据

打开月度账单时,把当前月的统计数据渲染到页面中
实现思路

  1. 使用useEffect函数
  2. 以当前时间为key值取账单数组
  3. monthResult自动重新计算

在data.json数据里新增两条2025年的,好对比效果

实现效果:

提交代码

9、月度账单-单日统计列表实现

实现思路

  1. 准备单日账单统计组件
  2. 把当前月的数据按照日来分组(日期列表和账单分组数据)
  3. 遍历数据给组件(传入日期数据和当日列表数据)

9-1 准备组件和配套样式

新建目录和文件

index.jsx 复制代码
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;
  }
}

组件导入

9-2 按日分组账单数据

9-3 遍历日账单组件并传入参数

测试

遍历

测试,实际在DayBill/index.jsx中还是假数据,还未替换Month中传过去的实际数据,此时我的2025-12月有两条数据,所以页面有渲染两条

9-4 接收数据计算统计渲染页面

在子组件接收父组件传递的数据,注意接收到的变量名要一一对应

提交

10、月度账单-单日账单列表展示

实现思路

  1. 准备列表模板
  2. 渲染模板数据
  3. 适配中文显示

10-1 渲染基础列表

复制代码
{/* 单日列表 */}
<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>

10-2 适配Type

将英文转成对应的中文

contants/index.jsx 复制代码
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
}, {})

提交

10-3 月度账单-切换打开关闭

实现思路

  1. 准备控制显隐状态
  2. 点击取反操作
  3. 根据状态适配箭头和显隐

提交

11、月度账单-Icon组件封装

需求:封装一个图标组件,可以根据不同的账单类型显示不同的图标
实现思路

  1. 准备纯静态的组件结构
  2. 根据不同的Props适配不同图标
icon/index.jsx 复制代码
const Icon = () => {
  return (
    <img
      src={`https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/ka/food.svg`}
      alt="icon"
      style={{
        width: 20,
        height: 20,
      }}
      />
  )
}

export default Icon

根据不同的Props适配不同图标

提交

12、记账功能

功能分析

  1. 不同类型的账单列表渲染
  2. 支出和收入两种状态切换
  3. 点击保存实现记账功能

12-1 记账 - 结构渲染

New/index.jsx 复制代码
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 { 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);
    }
    }
  }

12-2 记账 - 支出和收入切换

实现思路

  1. 准备控制收入和支出的状态
  2. 点击按钮切换状态
  3. 适配按钮样式
  4. 适配数据显示


12-3 记账 - 新增一笔

实现思路

  1. 组件中收集接口数据(type-账单类型/money-账单金额/date-记账时间/useFor-账单type
  2. 在Redux中编写异步代码
  3. 点击保存-提交action
  1. 收集接口数据

  1. 编写异步代码


优化

设置默认二级路由为月度账单


优化item.type选中状态

优化时间显示


提交代码

年度账单没有做

暂时完结撒花

相关推荐
|晴 天|1 小时前
前端安全入门:XSS 与 CSRF 的攻与防
前端·安全·xss
可爱又迷人的反派角色“yang”1 小时前
Mysql数据库(一)
运维·服务器·前端·网络·数据库·mysql·nginx
Aerelin1 小时前
爬虫图片采集(自动化)
开发语言·前端·javascript·爬虫·python·html
Highcharts.js1 小时前
Renko Charts|金融图表之“砖形图”
java·前端·javascript·金融·highcharts·砖型图·砖形图
含若飞1 小时前
列表弹窗实现方案整理
前端·javascript·vue.js
EB_Coder1 小时前
2025前端面试题-JavaScript基础篇
前端·javascript·面试
shaohaoyongchuang1 小时前
vue_05axios
前端·javascript·vue.js
f***14771 小时前
Node.js npm 安装过程中 EBUSY 错误的分析与解决方案
前端·npm·node.js
IT_陈寒1 小时前
React性能优化:5个90%开发者都会忽略的useEffect最佳实践
前端·人工智能·后端