环境搭建
使用CRA创建项目,并安装必要依赖,包括下列基础包
- Redux状态管理 - @reduxjs/toolkit 、 react-redux
- 路由 - react-router-dom
- 时间处理 - dayjs
- class类名处理 - classnames
- 移动端组件库 - antd-mobile
- 请求插件 - axios
pnpm i @reduxjs/toolkit react-redux react-router-dom classname dayjs antd-mobile axios
配置别名路径
1. 背景知识
- 路径解析配置(webpack),把 @/ 解析为 src/
- 路径联想配置(VsCode),VsCode 在输入 @/ 时,自动联想出来对应的 src/下的子级目录
2. 路径解析配置
配置步骤:
- 安装craco
npm i -D @craco/craco - 项目根目录下创建配置文件
craco.config.js - 配置文件中添加路径解析配置
- 包文件中配置启动和打包命令
const path = require("path");module.exports = {webpack: {alias: {"@": path.resolve(__dirname, "src"),},},
};
3. 联想路径配置
针对Vscode的配置
配置步骤:
- 根目录下新增配置文件 - jsconfig.json
- 添加路径提示配置
{"compilerOptions":{"baseUrl":"./","paths":{"@/*":["src/*"]}}
}
数据Mock实现
在前后端分类的开发模式下,前端可以在没有实际后端接口的支持下先进行接口数据的模拟,进行正常的业务功能开发
1. 常见的Mock方式
2. json-server实现Mock
实现步骤:
- 项目中安装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": -100,"date": "2023-04-04T03:03:15.617Z","useFor": "drinks","id": 19},{"type": "pay","money": -100,"date": "2023-04-02T16:00:00.000Z","useFor": "food","id": 20},{"type": "income","money": 10000,"date": "2023-02-28T16:00:00.000Z","useFor": "salary","id": 21}]
}
- 添加启动命令
"serve": "json-serve ./serve/data.json --port 8888"
- 访问接口进行测试
http://localhost:8888/ka
整体路由设计
俩个一级路由 (Layout / new)
俩个二级路由 (Layout - mouth/year)
步骤:
- Views 里面写简单的页面
const Month = () => {return <div>Month</div>;
};export default Month;
- router / index.js
import Layout from "@/views/Layout";
import Month from "@/views/Month";
import New from "@/views/New";
import Year from "@/views/Year";
import { createBrowserRouter } from "react-router-dom";const router = createBrowserRouter([{path: "/",element: <Layout />,children: [{path: "month",element: <Month />,},{path: "year",element: <Year />,},],},{path: "/new",element: <New />,},
]);export default router;
- 注入
根目录 index.js
配置路由的时候App可以不要了
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
// import App from "./App";
import router from "./router";
import { RouterProvider } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<RouterProvider router={router} />);
- 到这一步之前 2级路由 不会显示,因为还没配置2级路由出口!!!!
在year 和month的爹添加二级路由出口
import { Outlet } from "react-router-dom";const Layout = () => {return (<div>layout<Outlet /></div>);
};export default Layout;
antD主题定制
1. 定制方案
如果是全局就是div里面的Button和外边的Button都会生效;如果是局部的话,只有div里面的button才会变
2. 实现方式
- 创建css 文件
src / theme.css
/* 全局定制 */
:root:root {--adm-color-primary: skyblue;
}/* 局部定制 */
.purple {--adm-color-primary: purple;
}
- 导入 入口文件 index.js
// 导入定制主题
import "./theme.css";
- 使用
import { Outlet } from "react-router-dom";
import { Button } from "antd-mobile";
const Layout = () => {return (<div>layout{/* 注意这里的不是普通的button */}<Button color="primary">ceshi</Button><div className="purple"><Button color="primary">jvbu</Button></div><Outlet /></div>);
};export default Layout;
3. 记账本主题色
:root:root {--adm-color-primary: rgb(105, 174, 120);
}
Redux管理账目列表
// 账单列表相关storeimport { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'const billStore = createSlice({name: 'bill',// 数据状态stateinitialState: {billList: []},reducers: {// 同步修改方法setBillList (state, action) {state.billList = action.payload}}
})// 解构actionCreater函数
const { setBillList } = billStore.actions
// 编写异步
const getBillList = () => {return async (dispatch) => {// 编写异步请求const res = await axios.get('http://localhost:8888/ka')// 触发同步reducerdispatch(setBillList(res.data))}
}export { getBillList }
// 导出reducer
const reducer = billStore.reducerexport default reducer
// 组合子模块 导出store实例import { configureStore } from '@reduxjs/toolkit'
import billReducer from './modules/billStore'const store = configureStore({reducer: {bill: billReducer}
})export default store
入口文件 index.js
import router from './router'
import { Provider } from 'react-redux'const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Provider store={store}><RouterProvider router={router} /></Provider>
)
TabBar功能实现
1. 静态布局实现
配套静态模版和样式文件
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.scss'
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
.layout {.container {position: fixed;top: 0;bottom: 50px;}.footer {position: fixed;bottom: 0;width: 100%;}
}
下载sass
npm i -D sass
2. 切换路由实现
监听change事件,在事件回调中调用路由跳转方法
// 切换菜单跳转路由const navigate = useNavigate()const swithRoute = (path) => {console.log(path)navigate(path)}return (<div className="layout"><div className="footer"><TabBar onChange={swithRoute}>{/* 省略... */}</TabBar></div></div>)
月度账单-统计区域
1. 准备静态结构
views / Month / index.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>{/* 时间选择器 */}<DatePickerclassName="kaDate"title="记账日期"precision="month"visible={false}max={new Date()}/></div></div></div >)
}export default Month
.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;}}}
}
2. 点击切换时间选择框
实现思路:
- 准备一个状态数据
- 点击切换状态
- 根据状态控制弹框打开关闭以及箭头样式
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'
import { useState } from "react"
import classNames from "classnames"const Month = () => {// 控制时间选择器打开关闭const [dateVisible, setDateVisible] = useState(false)// 时间选择框确实事件const dateConfirm = (date) => {// 关闭弹框setDateVisible(false)}return (<div className="monthlyBill"><NavBar className="nav" backArrow={false}>月度收支</NavBar><div className="content"><div className="header">{/* 时间切换区域 */}<div className="date" onClick={() => setDateVisible(true)}>{/* 省略.. */}<span className={classNames('arrow', dateVisible && 'expand')}></span></div>{/* 统计区域 */}{/* 时间选择器 */}<DatePickerclassName="kaDate"title="记账日期"precision="month"visible={dateVisible}max={new Date()}onConfirm={dateConfirm}/></div></div></div >)
}export default Month
onClose是点击蒙层关闭
3. 切换时间显示
实现思路:
- 以当前时间作为默认值
- 在时间切换时完成时间修改
用dayjs实现格式化
import dayjs from "dayjs"const [currentMonth, setCurrentMonth] = useState(() => {return dayjs().format('YYYY-MM')
})const dateConfirm = (date) => {setDateVisible(false)const month = dayjs(date).format('YYYY-MM')setCurrentMonth(month)
}
4. 统计功能实现
实现思路:
- 按月分组
- 根据获取到的时间作为key取当月的账单数组
- 根据当月的账单数组计算支出、收入、总计
1. 按月分组
安装lodash
npm i lodash
使用
import _ from "lodash";
_.groupBy(billList,(item)=>item.data);
const billList = useSelector((state) => state.bill.billList);const groupList = useMemo(() => {return _.groupBy(billList, (item) => dayjs(item.date).format("YYYY-MM"));}, [billList]);console.log("groupList", groupList);
2. 根据获取到的时间作为key取当月的账单数组
3. 根据当月的账单数组计算支出、收入、总计
const [selectedMonthList, setelectedMonthList] = useState([]);const monthResult = useMemo(() => {console.log("monthResult里面的selectedMonthList", selectedMonthList);let income = selectedMonthList.filter((item) => item.type === "income").reduce((pre, cur) => pre + cur.money, 0);let pay = selectedMonthList.filter((item) => item.type === "pay").reduce((pre, cur) => pre + cur.money, 0);console.log(income);console.log(pay);return {pay,income,total: pay + income,};}, [selectedMonthList]);
4. 初始化时渲染统计数据(一打开页面就计算好当前月份的账单
useEffect(() => {const nowDate = dayjs().format("YYYY-MM");// 边界值控制// 因为是获取数据是异步的,所以 groupList[nowDate] 有可能是空的if (groupList[nowDate]) {setelectedMonthList(groupList[nowDate]);}}, [groupList]);
// 按月分组
// 注意这里的写法 state.bill.billList
const billList = useSelector(state => state.bill.billList)
const monthGroup = useMemo(() => {return _.groupBy(billList, item => dayjs(item.date).format('YYYY-MM'))
}, [billList])// 根据获取到的时间作为key取当月的账单数组
const dateConfirm = (date) => {const monthKey = dayjs(date).format('YYYY-MM')setMonthList(monthGroup[monthKey])
}// 计算统计
const overview = useMemo(() => {const income = currentMonthList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)const pay = currentMonthList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)return {income,pay,total: income + pay}
}, [currentMonthList])
5. 完整代码
import { useSelector } from "react-redux"
import { NavBar, DatePicker } from 'antd-mobile'
import './index.scss'
import _ from 'lodash'
import dayjs from "dayjs"
import { useMemo, useState } from "react"
import { useEffect } from "react"
import classNames from "classnames"const Month = () => {// 按月分组const billList = useSelector(state => state.bill.billList)const monthGroup = useMemo(() => {return _.groupBy(billList, item => dayjs(item.date).format('YYYY-MM'))}, [billList])// 控制时间选择器打开关闭const [dateVisible, setDateVisible] = useState(false)const [currentMonthList, setMonthList] = useState([])const [currentMonth, setCurrentMonth] = useState(() => {return dayjs().format('YYYY-MM')})const dateConfirm = (date) => {setDateVisible(false)const monthKey = dayjs(date).format('YYYY-MM')setCurrentMonth(monthKey)setMonthList(monthGroup[monthKey])}// 首次加载useEffect(() => {const list = monthGroup[dayjs().format('YYYY-MM')]if(list){setMonthList(list)}}, [monthGroup])// 计算统计const overview = useMemo(() => {if (!currentMonthList) return { income: 0, pay: 0, total: 0 }const income = currentMonthList.filter(item => item.type === 'income').reduce((a, c) => a + c.money, 0)const pay = currentMonthList.filter(item => item.type === 'pay').reduce((a, c) => a + c.money, 0)return {income,pay,total: income + pay}}, [currentMonthList])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">{currentMonth} 账单</span><span className={classNames('arrow', dateVisible && 'expand')}></span></div>{/* 统计区域 */}<div className='twoLineOverview'><div className="item"><span className="money">{overview.pay.toFixed(2)}</span><span className="type">支出</span></div><div className="item"><span className="money">{overview.income.toFixed(2)}</span><span className="type">收入</span></div><div className="item"><span className="money">{(overview.total).toFixed(2)}</span><span className="type">结余</span></div></div>{/* 时间选择器 */}<DatePickerclassName="kaDate"title="记账日期"precision="month"visible={dateVisible}max={new Date()}onConfirm={dateConfirm}/></div></div></div >)
}export default Month
月度账单-单日统计列表实现
views / Month / components / DayBill / index.js
1. 准备组件和配套样式
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
配套样式
.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;}
}
2. 按日分组账单数据
// 把当前月按日分组账单数据const dayGroup = useMemo(() => {const group = _.groupBy(currentMonthList, (item) => dayjs(item.date).format('YYYY-MM-DD'))return {dayKeys: Object.keys(group),group}}, [currentMonthList])console.log(dayGroup)
3. 遍历日账单组件并传入参数
{/* 日账单 */}
{dayGroup.dayKeys.map(dayKey => (<DailyBill key={dayKey} date={dayKey} billList={dayGroup.group[dayKey]} />
))}
4. 接收数据计算统计渲染页面
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
月度账单-单日账单列表展示
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>
2. 适配Type
1-准备静态数据
src / contants / index.js
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: '礼金收入' },],},],
}// 目的是将 billListData 中的数据转换为一个对象 billTypeToName,然后调用的时候就通过属性名来得到属性值
export const billTypeToName = Object.keys(billListData).reduce((prev, key) => {billListData[key].forEach(bill => {bill.list.forEach(item => {prev[item.type] = item.name})})return prev
}, {})
2-适配type
导入
import { billTypeToName } from "@/contants";
<div className="billType">{billTypeToName[item.useFor]}</div>
月度账单-切换打开关闭
// 声明状态
const [visible, setVisible] = useState(false)// 控制箭头<span className={classNames('arrow', !visible && 'expand')} onClick={() => setVisible(!visible)}></span>// 控制列表显示
<div className="billList" style={{ display: !visible && 'none' }}></div>
月度账单-Icon组件封装
src / components / Icon /index.js
1. 准备静态结构
const Icon = () => {return (<imgsrc={`https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/ka/food.svg`}alt="icon"style={{width: 20,height: 20,}}/>)
}export default Icon
2. 设计参数
传递参数:
<Icon type={item.useFor}></Icon>
const BASE_URL = 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/ka/'const Icon = ({ type }) => {return (<imgsrc={`${BASE_URL + type}.svg`}alt="icon"style={{width: 20,height: 20,}}/>)
}export default Icon
3. 使用组件
<div className="billList" style={{ display: visible ? 'block' : 'none' }}>{billList.map(item => {return (<div className="bill" key={item.id}><Icon type={item.useFor} /></div>)})}</div>
记账功能
记账 - 结构渲染
views/ New / index.js
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"><Buttonshape="rounded"className={classNames('selected')}>支出</Button><ButtonclassName={classNames('')}shape="rounded">收入</Button></div><div className="kaFormWrapper"><div className="kaForm"><div className="date"><Icon type="calendar" className="icon" /><span className="text">{'今天'}</span><DatePickerclassName="kaDate"title="记账日期"max={new Date()}/></div><div className="kaInput"><InputclassName="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 (<divclassName={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
配套样式
.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. 点击切换状态 */}<Buttonshape="rounded"className={classNames(billType==='pay'?'selected':'')}onClick={() => setBillType('pay')}>支出</Button><ButtonclassName={classNames(billType==='income'?'selected':'')}onClick={() => setBillType('income')}shape="rounded">收入</Button></div>{/* 2. 适配数据 */}<div className="kaTypeList">{billListData[billType].map(item => {})}</div></div>)
}
记账 - 新增一笔
1. 组件中收集接口数据
import { useDispatch } from 'react-redux'const New = () => {// 收集金额const [money, setMoney] = useState(0)const moneyChange = (value) => {setMoney(value)}// 收集账单类型const [useFor, setUseFor] = useState('')const dispatch = useDispatch()// 保存账单const saveBill = () => {// 收集表单数据const data = {type: billType,money: billType === 'pay' ? -money : +money,date: new Date(),useFor: useFor}console.log(data)dispatch(addBillList(data))}return (<div className="keepAccounts"><NavBar className="nav" onBack={() => navigate(-1)}>记一笔</NavBar><div className="header"><div className="kaType"><Buttonshape="rounded"className={classNames(billType === 'pay' ? 'selected' : '')}onClick={() => setBillType('pay')}>支出</Button><ButtonclassName={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">{'今天'}</span><DatePickerclassName="kaDate"title="记账日期"max={new Date()}/></div><div className="kaInput"><InputclassName="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 (<divclassName={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={saveBill}>保 存</Button></div></div>)
}export default New
2. 在Redux中编写异步代码
// 同步添加账单方法addBill(state,action){state.billList.push(action.payload)}//异步请求
const addBillList = (data) => {return async (dispatch) => {// 传递data数据const res = await axios.post("http://localhost:8888/ka", data);dispatch(addBill(res.data));};
};
3. 点击保存提交action
import { useDispatch } from "react-redux";// 点击按钮保存账单const saveBill = () => {console.log("money", money);const data = {type: billType,money: billType === "pay" ? -1 * money : money,data: new Date(),useFor: type,};console.log("data", data);dispatch(addBillList(data)); // 提交数据};
4. 选中的图标是激活状态
className={classNames("item",type === item.type && "selected")}key={item.type}
5. 时间选择
6. 如果日期是今天就显示“今天”而不是2024-04-15
const [selectedDate, setSelectedDate] = useState(new Date());<span className="text">{selectedDate === new Date()? "今天": dayjs(selectedDate).format("YYYY-MM-DD")}</span>
为什么二者不相等
在 JavaScript 中,两个对象即使它们包含相同的属性和值,但它们引用的内存地址不同,因此它们是不相等的。在这种情况下,selectedDate
是一个 Date
对象,new Date()
创建了一个新的 Date
对象,并且 selectedDate
也是一个 Date
对象,尽管它们的值可能相同,但它们引用的内存地址不同,所以它们是不相等的。
要比较两个日期对象的值是否相等,你应该比较它们的时间戳或者转换为字符串后再进行比较,例如:
selectedDate.getTime() === new Date().getTime()
或者:
selectedDate.toISOString() === new Date().toISOString()
这样可以确保比较的是它们的值而不是引用。