需要实现的交互效果
编辑的功能点击还是弹出新增账单窗口那个模块,不过需要稍微改动一下。
实现过程
1.封装公用的头部 Header
在 components 目录下新建 Header 目录,添加两个文件 index.jsx
和 style.module.less
。
import React from 'react'; import PropTypes from 'prop-types'; import { useNavigate } from 'react-router-dom' import { NavBar, Icon } from 'zarm'; import s from './style.module.less' const Header = ({ title = '' }) => { const navigate = useNavigate() return <div className={s.headerWarp}> <div className={s.block}> <NavBar className={s.header} left={<Icon type="arrow-left" theme="primary" onClick={() => navigate(-1)} />} title={title} /> </div> </div> }; Header.propTypes = { title: PropTypes.string, // 标题 }; export default Header;
.header-warp { border-bottom: 1px solid #e9e9e9; .block { width: 100%; height: 46px; :global { .za-nav-bar__title { font-size: 14px; color: rgba(0, 0, 0, 0.9); } .za-icon--arrow-left { font-size: 20px; } } } .header { position: fixed; top: 0; left: 0; width: 100%; .more { font-size: 20px; } } }
2.安装 query-string
npm i query-string -s
用法请参考:https://github.com/sindresorhus/query-string
const queryString = require('query-string'); console.log(location.search); //=> '?foo=bar' const parsed = queryString.parse(location.search); console.log(parsed); //=> {foo: 'bar'} console.log(location.hash); //=> '#token=bada55cafe' const parsedHash = queryString.parse(location.hash); console.log(parsedHash); //=> {token: 'bada55cafe'} parsed.foo = 'unicorn'; parsed.ilike = 'pizza'; const stringified = queryString.stringify(parsed); //=> 'foo=unicorn&ilike=pizza' location.search = stringified; // note that `location.search` automatically prepends a question mark console.log(location.search); //=> '?foo=unicorn&ilike=pizza'
3.添加 Detail 路由
在 src\router\index.js
里添加路由:
import Login from '@/container/Login' import Home from '@/container/Home' import Data from '@/container/Data' import User from '@/container/User' import Detail from '@/container/Detail' const routes = [ { path: "/login", component: Login },{ path: "/", component: Home },{ path: "/data", component: Data },{ path: "/user", component: User },{ path: "/detail", component: Detail } ]; export default routes
4.添加 Detail 模块代码
在 container 目录下新建 Detail 目录,添加文件 index.jsx
和 style.module.less
,以及 api/index.js
接口配置文件。
import React, { useEffect, useState, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Modal, Toast } from 'zarm'; import qs from 'query-string'; import cx from 'classnames'; import Header from '@/components/Header'; import CustomIcon from '@/components/CustomIcon'; import PopupAddBill from '@/components/PopupAddBill'; import { typeMap } from '@/utils'; import { billDetails, billDelete } from './api/index.js'; import s from './style.module.less'; const Detail = () => { const editRef = useRef(); const navigate = useNavigate(); const location = useLocation(); // 路由 location 实例 const { id } = qs.parse(location.search); // 查询字符串反序列化 const [detail, setDetail] = useState({}); // 订单详情数据 useEffect(() => { getDetail() }, []); const getDetail = async () => { const { data } = await billDetails({ id }); setDetail(data); } // 删除方法 const deleteDetail = () => { Modal.confirm({ title: '删除', content: '确认删除账单?', onOk: async () => { const { data } = await billDelete({ id }) Toast.show('删除成功') navigate(-1) }, }); } return <div className={s.detail}> <Header title='账单详情' /> <div className={s.card}> <div className={s.type}> {/* 通过 pay_type 属性,判断是收入或指出,给出不同的颜色*/} <span className={cx({ [s.expense]: detail.pay_type == 1, [s.income]: detail.pay_type == 2 })}> {/* typeMap 是我们事先约定好的 icon 列表 */} <CustomIcon className={s.iconfont} type={detail.type_id ? typeMap[detail.type_id].icon : 1} /> </span> <span>{ detail.type_name || '' }</span> </div> { detail.pay_type == 1 ? <div className={cx(s.amount, s.expense)}>-{ detail.amount }</div> : <div className={cx(s.amount, s.incom)}>+{ detail.amount }</div> } <div className={s.info}> <div className={s.time}> <span>记录时间</span> <span>{detail.date}</span> </div> <div className={s.remark}> <span>备注</span> <span>{ detail.remark || '-' }</span> </div> </div> <div className={s.operation}> <span onClick={deleteDetail}><CustomIcon type='shanchu' />删除</span> <span onClick={() => editRef.current && editRef.current.show()}><CustomIcon type='tianjia' />编辑</span> </div> </div> <PopupAddBill ref={editRef} detail={detail} onReload={getDetail} /> </div> } export default Detail
.detail { height: 100%; display: flex; flex-direction: column; background-color: #f5f5f5; padding: 12px 24px 0 24px; } .card { border-radius: 12px; background-color: #fff; padding: 0 12px; display: flex; flex-direction: column; align-items: center; .type { padding: 24px 0 12px 0; span:nth-of-type(1) { display: inline-block; width: 22px; height: 22px; color: #fff; border-radius: 50%; text-align: center; line-height: 24px; margin-right: 8px; } .expense { background-color: #007fff; } .income { background-color: rgb(236, 190, 37); } .iconfont { font-size: 16px; } } .amount { font-size: 24px; font-weight: 600; margin-bottom: 24px; } .info { width: 100%; font-size: 14px; text-align: left; .time { display: flex; align-items: center; justify-content: flex-start; margin-bottom: 12px; span:nth-of-type(1) { flex: 3; color: rgba(0,0,0,0.5) } span:nth-of-type(2) { flex: 9; } } .remark { display: flex; align-items: center; justify-content: flex-start; margin-bottom: 12px; span:nth-of-type(1) { flex: 3; color: rgba(0,0,0,0.5) } span:nth-of-type(2) { flex: 9; color: rgba(0,0,0,0.9) } } } .operation { width: 100%; height: 50px; display: flex; align-items: center; font-size: 16px; .van-icon { margin-right: 4px; } span { display: flex; align-items: center; justify-content: center; height: 100%; flex: 1 } span:nth-of-type(1) { color: red; } } }
import { fetchData } from "@/utils/axios.js"; // 获取账单详情 export function billDetails(data) { return fetchData('/api/bill/details', 'get', data); } // 删除账单 export function billDelete(data) { return fetchData('/api/bill/delete', 'post', data); }
5.更新 PopupAddBill 模块逻辑
里面添加了编辑功能的相关逻辑,以及接口的配置
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, billAdd, billUpdate } from './api/index.js' import { typeMap } from '@/utils'; import cx from 'classnames'; import s from './style.module.less'; const PopupAddBill = forwardRef(({ detail = {}, onReload }, ref) => { const dateRef = useRef(); const id = detail && detail.id // 外部传进来的账单详情 id 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); // 备注输入框 // 详情信息回显 useEffect(() => { if (detail.id) { setPayType(detail.pay_type == 1 ? 'expense' : 'income') setCurrentType({ id: detail.type_id, name: detail.type_name }) setRemark(detail.remark) setAmount(detail.amount) setDate(detail.date) } }, [detail]) // 通过 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); // 没有 id 的情况下,说明是新建账单。 if(!id) { 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 || '' } // 如果有id,调用编辑账单,没有就新增账单 if(id) { const { status, desc, data } = await billUpdate({ id, ...params }); console.log('编辑账单', status, desc, data); if(status === 200) { Toast.show('编辑成功'); }else{ Toast.show(desc); return ; } }else{ const { status, desc, data } = await billAdd(params); console.log('新增账单', status, desc, data); if(status === 200) { Toast.show('新增成功'); }else{ Toast.show(desc); return ; } // 重置参数 setAmount(''); setPayType('expense'); setCurrentType(expense[0]); setDate(new Date()); setRemark(''); Toast.show('添加成功'); } setShow(false); // 刷新数据 if (onReload) 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> }) PopupAddBill.propTypes = { detail: PropTypes.object, onReload: PropTypes.func } export default PopupAddBill
在 api/index.js
里添加编辑更新账单接口
import { fetchData } from "@/utils/axios.js"; // 获取类型字典列表 export function queryTypeList(data) { return fetchData('/api/type/list', 'get', data); } // 添加账单 export function billAdd(data) { return fetchData('/api/bill/add', 'post', data); } // 更新账单信息 export function billUpdate(data) { return fetchData('/api/bill/update', 'post', data); }
6.测试一下效果
我们从列表页随便选择一个账单,点击进去
详情页面如下
下面我们先测试删除按钮,我们点击删除按钮
点击确定:我们发现删除成功了
我们在测试一下编辑功能:我们将数据改变一下
点击确认:我们发现数据更新了
回到列表页:我们也发现更新成功了。