需要实现的交互效果
大致如下图:
实现过程
1.先编写新增按钮
先实现点击新增按钮,调出弹窗的功能。
在 src\container\Home\index.jsx
里面添加下面的代码
import CustomIcon from '@/components/CustomIcon' import PopupAddBill from '@/components/PopupAddBill' ... const Home = () => { ... const addRef = useRef(); // 新增账单 ref const addToggle = () => { addRef.current && addRef.current.show() } ... return <div className={s.home}> <div className={s.contentWrap}> <PopupAddBill ref={addRef} onReload={refreshData} /> </div> ... <div className={s.add} onClick={addToggle}><CustomIcon type='bianji' /></div> </div> }
在 src\container\Home\style.module.less
添加样式
给 border 设置的是 1PX,大写的单位,因为这样写的话,postcss-pxtorem 插件就不会将其转化为 rem 单位。
.home { ... .add { position: fixed; bottom: 100px; right: 30px; z-index: 1000; width: 50px; height: 50px; border-radius: 50%; box-shadow: 0 0 10px 0 rgb(0 0 0 / 20%); background-color: #fff; display: flex; justify-content: center; align-items: center; border: 1PX solid #e9e9e9; color: #007fff; } }
我们就能得到下面的效果
2.添加datatime日期选择类型
在 src\components\PopupDate\index.jsx
添加 datetime 类型
const choseMonth = (item) => { setNow(item) setShow(false) if (mode == 'month') { onSelect(dayjs(item).format('YYYY-MM')) } else if (mode == 'date') { onSelect(dayjs(item).format('YYYY-MM-DD')) } else if (mode == 'datetime') { onSelect(dayjs(item).format('YYYY-MM-DD HH:mm:ss')) } }
用于实现下面的效果:
3.实现新增账单弹窗封装
在 components 下新建 PopupAddBill 文件夹,再新建 index.jsx 和 style.module.less,代码如下:
账单类型和账单时间
将金额动态化,引入 Zarm 为我们提供的模拟数字键盘组件 Keyboard,这里我们使用 “zarm”: “^2.8.2”,新版本好像有点问题
import React, { forwardRef, useEffect, useRef, useState } from 'react'; import { Popup, Icon, Keyboard, Input, Toast } from 'zarm'; import CustomIcon from '@/components/CustomIcon' import PopupDate from '../PopupDate' import dayjs from 'dayjs'; import PropTypes from 'prop-types'; import { queryTypeList, addBillData } from './api/index.js' import { typeMap } from '@/utils'; import cx from 'classnames'; import s from './style.module.less'; const PopupAddBill = forwardRef((props, ref) => { const dateRef = useRef(); const [show, setShow] = useState(false) // 内部控制弹窗显示隐藏。 const [date, setDate] = useState(new Date()); // 日期 const [payType, setPayType] = useState('expense'); // 支出或收入类型 const [currentType, setCurrentType] = useState({}); // 当前选中账单类型 const [amount, setAmount] = useState(''); // 账单金额 const [expense, setExpense] = useState([]); // 支出类型数组 const [income, setIncome] = useState([]); // 收入类型数组 const [remark, setRemark] = useState(''); // 备注 const [showRemark, setShowRemark] = useState(false); // 备注输入框 // 通过 forwardRef 拿到外部传入的 ref,并添加属性,使得父组件可以通过 ref 控制子组件。 if (ref) { ref.current = { show: () => { setShow(true); }, close: () => { setShow(false); } } }; useEffect(async () => { const { data } = await queryTypeList({}); const _expense = data.filter(i => i.type == 1); // 支出类型 const _income = data.filter(i => i.type == 2); // 收入类型 setExpense(_expense); setIncome(_income); setCurrentType(_expense[0]); // 新建账单,类型默认是支出类型数组的第一项 }, []) // 切换收入还是支出 const changeType = (type) => { setPayType(type); type == 'expense' ? setCurrentType(expense[0]) : setCurrentType(income[0]); }; // 日期选择回调 const selectDate = (val) => { console.log('日期选择回调', val) setDate(val); } // 监听输入框改变值 const handleMoney = (value) => { console.log('value', value) value = String(value) // 点击关闭的时候 if(value == 'close') { setShow(false) return } // 点击是删除按钮时 if (value == 'delete') { let _amount = amount.slice(0, amount.length - 1) setAmount(_amount) return } // 点击确认按钮时,调用新增账单接口 if (value == 'ok') { addBill() return } // 当输入的值为 '.' 且 已经存在 '.',则不让其继续字符串相加。 if (value == '.' && amount.includes('.')) return // 小数点后保留两位,当超过两位时,不让其字符串继续相加。 if (value != '.' && amount.includes('.') && amount && amount.split('.')[1].length >= 2) return // amount += value setAmount(amount + value) } // 添加账单 const addBill = async () => { if (!amount) { Toast.show('请输入具体金额') return } const params = { amount: Number(amount).toFixed(2), type_id: currentType.id, type_name: currentType.name, date: dayjs(date).format('YYYY-MM-DD HH:mm:ss'), pay_type: payType == 'expense' ? 1 : 2, remark: remark || '' } const result = await addBillData(params); console.log(result); // 重置参数 setAmount(''); setPayType('expense'); setCurrentType(expense[0]); setDate(new Date()); setRemark(''); Toast.show('添加成功'); setShow(false); // 刷新数据 if (props.onReload) props.onReload(); } return <Popup visible={show} direction="bottom" onMaskClick={() => setShow(false)} destroy={false} mountContainer={() => document.body} > <div className={s.addWrap}> {/* 右上角关闭弹窗 */} <header className={s.header}> <span className={s.close} onClick={() => setShow(false)}><Icon type="wrong" /></span> </header> {/* 「收入」和「支出」类型切换 */} <div className={s.filter}> <div className={s.type}> <span onClick={() => changeType('expense')} className={cx({ [s.expense]: true, [s.active]: payType == 'expense' })}>支出</span> <span onClick={() => changeType('income')} className={cx({ [s.income]: true, [s.active]: payType == 'income' })}>收入</span> </div> <div className={s.time} onClick={() => dateRef.current && dateRef.current.show()} >{dayjs(date).format('YYYY-MM-DD HH:mm')} <Icon className={s.arrow} type="arrow-bottom" /></div> </div> <div className={s.money}> <span className={s.sufix}>¥</span> <span className={cx(s.amount, s.animation)}>{amount}</span> </div> <div className={s.typeWarp}> <div className={s.typeBody}> {/* 通过 payType 判断,是展示收入账单类型,还是支出账单类型 */} { (payType == 'expense' ? expense : income).map(item => <div onClick={() => setCurrentType(item)} key={item.id} className={s.typeItem}> {/* 收入和支出的字体颜色,以及背景颜色通过 payType 区分,并且设置高亮 */} <span className={cx({[s.iconfontWrap]: true, [s.expense]: payType == 'expense', [s.income]: payType == 'income', [s.active]: currentType.id == item.id})}> <CustomIcon className={s.iconfont} type={typeMap[item.id].icon} /> </span> <span>{item.name}</span> </div>) } </div> </div> <div className={s.remark}> { showRemark ? <Input autoHeight showLength maxLength={50} type="text" rows={3} value={remark} placeholder="请输入备注信息" onChange={(val) => setRemark(val)} onBlur={() => setShowRemark(false)} /> : <span onClick={() => setShowRemark(true)}>{remark || '添加备注'}</span> } </div> <Keyboard type="price" onKeyClick={(value) => handleMoney(value)} /> <PopupDate ref={dateRef} mode="datetime" onSelect={selectDate} /> </div> </Popup> }) export default PopupAddBill
.add-wrap { padding-top: 12px; background-color: #fff; border-top-left-radius: 10px; border-top-right-radius: 10px; .header { padding: 0 16px; .close { display: flex; align-items: center; justify-content: flex-end; } } .filter { padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; .type { span { display: inline-block; background: #f5f5f5; color: rgba(0, 0, 0, 0.5); padding: 4px 12px; font-size: 12px; border-radius: 24px; border: 1px solid #f5f5f5; } .expense { margin-right: 6px; &.active { background-color: #eafbf6; border-color: #007fff; color: #007fff; } } .income { &.active { background-color: #fbf8f0; border-color: rgb(236, 190, 37); color: rgb(236, 190, 37); } } } } .time { display: flex; justify-content: center; align-items: center; padding: 4px 12px; background-color: #f0f0f0; border-radius: 20px; color: rgba(0, 0, 0, 0.9); .arrow { font-size: 12px; margin-left: 5px; } } .money { padding-bottom: 12px; border-bottom: 1px solid #e9e9e9; margin: 0 24px; .sufix { font-size: 36px; font-weight: bold; vertical-align: top; } .amount { font-size: 40px; padding-left: 10px; } } .type-warp { display: flex; overflow-x: auto; margin: 0 24px; margin-bottom: 20px; * { touch-action: pan-x; } .type-body { display: flex; white-space: nowrap; .type-item { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 16px 12px 10px 12px; .iconfont-wrap { display: flex; justify-content: center; align-items: center; background-color: #f5f5f5; border-radius: 50%; width: 30px; height: 30px; margin-bottom: 5px; .iconfont { color: rgba(0, 0, 0, 0.5); font-size: 20px; } } .expense { &.active { background-color: #007fff; .iconfont { color: #fff; } } } .income { &.active { background-color: rgb(236, 190, 37); .iconfont { color: #fff; } } } } } } .remark { padding: 0 24px; padding-bottom: 12px; color: #4b67e2; :global { .za-input--textarea { border: 1px solid #e9e9e9; padding: 10px; } } } }
4.测试效果
我们先点新增按钮
然后就会弹出新增账单的窗口
我们测试一下支出的(收入可以自己去测试一下)
填写好信息之后,我们点击确定,发现新增支出的账单数据就添加成功了。
另外点击日期时间选择:我改成了精确到分钟