记账本
- 1、环境搭建
- 2、配置别名路径
-
- [2-1 背景知识](#2-1 背景知识)
- [2-2 路径解析配置](#2-2 路径解析配置)
- [2-3 联想路径配置](#2-3 联想路径配置)
- 3、数据Mock实现
-
- [3-1 常见的Mock方式](#3-1 常见的Mock方式)
- [3-2 json-server实现Mock](#3-2 json-server实现Mock)
- 4、整体路由设计
- 5、antD主题定制
-
- [5-1 定制方案](#5-1 定制方案)
- [5-2 实现方式](#5-2 实现方式)
- 6、Redux管理账目列表
- 7、TabBar功能实现
-
- [7-1 静态布局实现](#7-1 静态布局实现)
- [7-2 切换路由实现](#7-2 切换路由实现)
- 8、月度账单-统计区域
-
- [8-1 准备静态结构](#8-1 准备静态结构)
- [8-2 点击切换时间选择框](#8-2 点击切换时间选择框)
-
- [Picker 选择器](#Picker 选择器)
- [8-3 切换时间显示](#8-3 切换时间显示)
- [8-4 统计功能实现](#8-4 统计功能实现)
- 9、月度账单-单日统计列表实现
-
- [9-1 准备组件和配套样式](#9-1 准备组件和配套样式)
- [9-2 按日分组账单数据](#9-2 按日分组账单数据)
- [9-3 遍历日账单组件并传入参数](#9-3 遍历日账单组件并传入参数)
- [9-4 接收数据计算统计渲染页面](#9-4 接收数据计算统计渲染页面)
- 10、月度账单-单日账单列表展示
-
- [10-1 渲染基础列表](#10-1 渲染基础列表)
- [10-2 适配Type](#10-2 适配Type)
- [10-3 月度账单-切换打开关闭](#10-3 月度账单-切换打开关闭)
- 11、月度账单-Icon组件封装
- 12、记账功能
-
- [12-1 记账 - 结构渲染](#12-1 记账 - 结构渲染)
- [12-2 记账 - 支出和收入切换](#12-2 记账 - 支出和收入切换)
- [12-3 记账 - 新增一笔](#12-3 记账 - 新增一笔)
- 优化
1、环境搭建
使用CRA/vite创建项目,并安装必要依赖,包括下列基础包
- Redux状态管理 - @reduxjs/toolkit 、 react-redux
- 路由 - react-router-dom
- 时间处理 - dayjs
- class类名处理 - classnames
- 移动端组件库 - antd-mobile
- 请求插件 - axios

检查

清理文件



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

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


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

2-2 路径解析配置
配置步骤:(使用
CRA创建项目需要进行以下操作)
- 安装craco npm i -D @craco/craco
- 项目根目录下创建配置文件 craco.config.js
- 配置文件中添加路径解析配置
- 包文件中配置启动和打包命令

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创建都需要)
- 根目录下新增配置文件 - jsconfig.json
- 添加路径提示配置
json
{
"compilerOptions":{
"baseUrl":"./",
"paths":{
"@/*":[
"src/*"
]
}
}
}
完成之后提交一次代码

3、数据Mock实现
3-1 常见的Mock方式
其实就是模拟假数据,在没有实际后端接口的支持下,进行正常业务开发

3-2 json-server实现Mock
json-server是一个node包
实现步骤:
- 项目中安装json-server npm i -D json-server
- 准备一个json文件 (素材里获取)
- 添加启动命令
- 访问接口进行测试
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、整体路由设计
- 俩个一级路由 (Layout / new)
- 俩个二级路由 (Layout - mouth/year)
实践:


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



创建路由实例,绑定path element

引入组件

5、antD主题定制
5-1 定制方案

5-2 实现方式
- 全局定制
src/theme.css
/* 全局定制 */
:root:root {
--adm-color-primary: rgb(105, 174, 120);
}
- 局部定制
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管理账目列表

- 创建文件

- 编写账单相关store

- 组合子模块 导出store实例

4.导入store

- 触发异步请求

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.Item的key值会被作为回调参数传递
测试
完成后提交代码

8、月度账单-统计区域
实现以下效果

功能点
- 点击切换月份
- 适配箭头显示
- 统计指出、收入、结余数据
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 点击切换时间选择框
功能要求:
- 点击打开时间选择弹框
- 点击取消/确认按钮以及蒙层区域都可以关闭弹框
- 弹框关闭时箭头朝下,打开是箭头朝上
实现思路:
- 准备一个状态数据
- 点击切换状态
- 根据状态控制弹框打开关闭以及箭头样式

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

8-3 切换时间显示
实现思路
- 创建一个控制时间显示的状态
- 拿到当前选中的时间赋值给状态
实现效果
提交代码
8-4 统计功能实现
实现思路:
- 按月分组
- 根据获取到的时间作为key取当月的账单数组
- 根据当月的账单数组计算支出、收入、总计
按月分组得到当月账单数组
npm i lodash
例子
_.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是满足某一个分组要求的所有元素

提交代码

计算选择月份的统计数据
点击时间确认按钮之后,把当前月份的统计数据计算出来显示到页面中
实现思路
- 点击确认获取到当前月份
- 按月分组数据中找到对应数组
- 基于数组做计算,使用useMemo-reduce


月度初始化时渲染统计数据
打开月度账单时,把当前月的统计数据渲染到页面中
实现思路
- 使用useEffect函数
- 以当前时间为key值取账单数组
- monthResult自动重新计算
在data.json数据里新增两条2025年的,好对比效果


实现效果:

提交代码

9、月度账单-单日统计列表实现
实现思路
- 准备单日账单统计组件
- 把当前月的数据按照日来分组(日期列表和账单分组数据)
- 遍历数据给组件(传入日期数据和当日列表数据)
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、月度账单-单日账单列表展示
实现思路
- 准备列表模板
- 渲染模板数据
- 适配中文显示
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 月度账单-切换打开关闭
实现思路
- 准备控制显隐状态
- 点击取反操作
- 根据状态适配箭头和显隐

提交

11、月度账单-Icon组件封装
需求:封装一个图标组件,可以根据不同的账单类型显示不同的图标
实现思路
- 准备纯静态的组件结构
- 根据不同的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、记账功能
功能分析
- 不同类型的账单列表渲染
- 支出和收入两种状态切换
- 点击保存实现记账功能
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 记账 - 支出和收入切换
实现思路
- 准备控制收入和支出的状态
- 点击按钮切换状态
- 适配按钮样式
- 适配数据显示


12-3 记账 - 新增一笔
实现思路
- 组件中收集接口数据(type-账单类型/money-账单金额/date-记账时间/useFor-账单type
- 在Redux中编写异步代码
- 点击保存-提交action
- 收集接口数据



- 编写异步代码



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


优化item.type选中状态

优化时间显示



提交代码
年度账单没有做
暂时完结撒花














