背景 : react 项目
问题 : 在折扣数中输入折扣2.333333,中间会多很多0,输入2.222,不能正常输入到第三位
如下图
原因 : toFixed()数字转字符串时可能会导致精度问题
解决思路 : parseFloat来解析浮点数,Number.isFinite判断给定的值是否为有限数值
原代码
const calculateDiscountVal = (price, vip_price) => {
if (Number(price) > 0 && Number(vip_price) > 0) {
return (Number(vip_price) / Number(price)).toFixed(2) * 10;
} else {
return 0;
}
};
const updateVipPriceFromDiscountRate = (price, discountRate) => {
if (Number(price) > 0 && Number(discountRate) >= 0 && Number(discountRate) <= 10) {
const vip_price = price * (discountRate / 10).toFixed(2);
form.setFieldsValue({ vip_price: vip_price });
}
};
useEffect(() => {
// 只在零售价改变时更新折扣率
let price = form.getFieldValue('price');
let vip_price = form.getFieldValue('vip_price');
setDiscountVal(calculateDiscountVal(price, vip_price));
}, [form.getFieldValue('price'), form.getFieldValue('vip_price')]);
// 问题:输入3.3折后,输入框变成3.30000 ; 原因:toFixed()数字转字符串时可能会导致精度问题 ; 解决:parseFloat来解析浮点数
const handleDiscountRateChange = (ev) => {
const newVal = ev.target.value;
if (Number(newVal) >= 0 && Number(newVal) <= 10) {
setDiscountVal(newVal);
const price = form.getFieldValue('price');
updateVipPriceFromDiscountRate(price, newVal);
}
};
return (
<Form >
<Form.Item label='零售价(元):' name='price'>
<Input
value={price}
onChange={(e) => setprice(e.target.value)}
autoComplete='off'
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item label='折扣价(元):'>
<Form.Item name='vip_price' style={{ display: 'inline-block' }}>
<Input
value={vip_price}
onChange={(e) => setvip_price(e.target.value)}
autoComplete='off'
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item style={{ display: 'inline-block', margin: '0 8px' }}>
<Input
value={discountVal}
allowClear
onChange={handleDiscountRateChange}
autoComplete='off'
style={{ width: 80 }}
></Input>
折
</Form.Item>
</Form.Item>
</>
)}
<Form.Item wrapperCol={{ offset: 2 }}>
<Space className='footer' size={20}>
<Button
onClick={() => {
navigate(-1);
}}
>
取消
</Button>
<Button type='primary' htmlType='submit'>
确认
</Button>
</Space>
</Form.Item>
<<Form />
解决方法 1 : 上述其他代码不变 , 只在handleDiscountRateChange中加入parseFloat , 不可行 , 还会出现NaN , 如图
// 在onChange中用parseFloat来解析浮点数,输入折扣数后,就触发折扣价改变,发现还是不太行,因为浮点数的精度问题可能会导致在 onChange 事件中出现意外行为。目前会更报错出现NaN,当在用户输入小数点和小数部分时,浮点数可能会被不完整地解析,导致计算错误,出现NaN
const handleDiscountRateChange = (ev) => {
const newVal = ev.target.value;
if (Number(newVal) >= 0 && Number(newVal) <= 10) {
const roundedVal = parseFloat(newVal).toFixed(2); // 对输入的值进行四舍五入处理
setDiscountVal(roundedVal);
const price = form.getFieldValue('price');
// 添加计算并更新折扣金额的逻辑
if (Number(price) > 0) {
const vip_price = price * (roundedVal / 10);
form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
}
}
};
最终解决方案 : 将parseFloat放在失去焦点(onBlur)中处理,更靠谱 , 如图
// 将parseFloat放在失去焦点(onBlur)中处理,更靠谱
const handleDiscountRateChange = (ev) => {
const newVal = ev.target.value;
if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// 不在这里进行四舍五入处理,而是在失去焦点时处理
setDiscountVal(newVal);
}
};
const handleDiscountRateBlur = () => {
const newVal = parseFloat(discountVal);
if (Number.isFinite(newVal) && newVal >= 0 && newVal <= 10) {
const roundedVal = newVal.toFixed(2);
setDiscountVal(roundedVal);
const price = form.getFieldValue('price');
// 添加计算并更新折扣元的逻辑
if (Number(price) > 0) {
const vip_price = price * (roundedVal / 10);
form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
}
}
};
<Form.Item style={{ display: 'inline-block', margin: '0 8px' }}>
<Input
value={discountVal}
allowClear
onChange={handleDiscountRateChange}
onBlur={handleDiscountRateBlur} // 添加失去焦点事件处理函数
autoComplete='off'
style={{ width: 80 }}
></Input>
折
</Form.Item>
----------------------上述原代码不完整,以提供思路解决完问题为主,以下是完整的代码---------------------
import React, { useEffect, useState, useRef } from 'react';
import {
Form,
Input,
Select,
Button,
Radio,
Space,
Image,
Spin,
Tree,
Drawer,
Row,
Col,
} from 'antd';
import { getInfoById, publisherList, publisherAdd, getListAll, editBook } from './request';
import { getList } from '../classify/request';
import { useLocation } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { convertToAntdTreeData } from '@/utils/common.js';
const SaleEdit = () => {
const {
state: { id },
} = useLocation();
const [initialValues, setinitialValues] = useState({
name: '',
authors: '',
category_id: null,
producers_id: null,
press_id: null,
is_free: null,
});
const [form] = Form.useForm();
const navigate = useNavigate();
const [showAddCategory, setShowAddCategory] = useState(false); //弹出添加出品方
const [showAddCategory1, setShowAddCategory1] = useState(false);
const [showDrawer, setShowDrawer] = useState(false);
const [publisher_name, setPublisherName] = useState(''); //出品方
const [publishers, setPublishers] = useState([]);
const [publisher_name1, setPublisherName1] = useState(''); //出版社
const [publishers1, setPublishers1] = useState([]);
const [treeData, setTreeData] = useState([]);
// 在组件中添加一个状态来保存书籍类型的值
const [bookType, setBookType] = useState(0); // 默认值为免费
const [producersConfig, setproducersConfig] = useState({
validateStatus: '',
help: '',
noStyle: false,
style: {},
});
const [pressConfig, setpressConfig] = useState({
validateStatus: '',
help: '',
noStyle: false,
style: {},
});
const addproducers = async (obj, callbackFn) => {
const { types } = obj;
let flag = true;
if (types == 1) {
if (!publisher_name) {
flag = false;
setproducersConfig({
...producersConfig,
validateStatus: 'error',
help: '请输入新的出品方',
noStyle: false,
});
}
} else if (types == 2) {
if (!publisher_name1) {
flag = false;
setpressConfig({
...pressConfig,
validateStatus: 'error',
help: '请输入新的出版社',
noStyle: false,
});
}
}
if (!flag) return;
// 上面代码是校验
const bool = await publisherAdd(obj);
if (!bool) return;
callbackFn();
setproducersConfig({ validateStatus: '', help: '', noStyle: false, style: {} });
setpressConfig({ validateStatus: '', help: '', noStyle: false, style: {} });
getProducers();
};
const [price, setprice] = useState(0);
const [vip_price, setvip_price] = useState(0);
const [discountVal, setDiscountVal] = useState(0);
const calculateDiscountVal = (price, vip_price) => {
if (Number(price) > 0 && Number(vip_price) > 0) {
return (Number(vip_price) / Number(price)).toFixed(2) * 10;
} else {
return 0;
}
};
const updateVipPriceFromDiscountRate = (price, discountRate) => {
if (Number(price) > 0 && Number(discountRate) >= 0 && Number(discountRate) <= 10) {
const vip_price = price * (discountRate / 10).toFixed(2);
form.setFieldsValue({ vip_price: vip_price });
}
};
useEffect(() => {
// 只在零售价改变时更新折扣率
let price = form.getFieldValue('price');
let vip_price = form.getFieldValue('vip_price');
setDiscountVal(calculateDiscountVal(price, vip_price));
}, [form.getFieldValue('price'), form.getFieldValue('vip_price')]);
// // 问题:输入3.3折后,输入框变成3.30000 ; 原因:toFixed()数字转字符串时可能会导致精度问题 ; 解决:parseFloat来解析浮点数
// const handleDiscountRateChange = (ev) => {
// const newVal = ev.target.value;
// if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// setDiscountVal(newVal);
// const price = form.getFieldValue('price');
// updateVipPriceFromDiscountRate(price, newVal);
// }
// };
// 在onChange中用parseFloat来解析浮点数,输入折扣数后,就触发折扣价改变,发现还是不太行,因为浮点数的精度问题可能会导致在 onChange 事件中出现意外行为。目前会更报错出现NaN,当在用户输入小数点和小数部分时,浮点数可能会被不完整地解析,导致计算错误,出现NaN
// const handleDiscountRateChange = (ev) => {
// const newVal = ev.target.value;
// if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// const roundedVal = parseFloat(newVal).toFixed(2); // 对输入的值进行四舍五入处理
// setDiscountVal(roundedVal);
// const price = form.getFieldValue('price');
// // 添加计算并更新折扣金额的逻辑
// if (Number(price) > 0) {
// const vip_price = price * (roundedVal / 10);
// form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
// }
// }
// };
// 将parseFloat放在失去焦点(onBlur)中处理,更靠谱
const handleDiscountRateChange = (ev) => {
const newVal = ev.target.value;
if (Number(newVal) >= 0 && Number(newVal) <= 10) {
// 不在这里进行四舍五入处理,而是在失去焦点时处理
setDiscountVal(newVal);
}
};
const handleDiscountRateBlur = () => {
const newVal = parseFloat(discountVal);
if (Number.isFinite(newVal) && newVal >= 0 && newVal <= 10) {
const roundedVal = newVal.toFixed(2);
setDiscountVal(roundedVal);
const price = form.getFieldValue('price');
// 添加计算并更新折扣元的逻辑
if (Number(price) > 0) {
const vip_price = price * (roundedVal / 10);
form.setFieldsValue({ vip_price: vip_price.toFixed(2) });
}
}
};
useEffect(() => {
if (!showAddCategory) {
setPublisherName('');
}
if (!showAddCategory1) {
setPublisherName1('');
}
}, [showAddCategory, showAddCategory1]);
const handleOpenDrawer = async () => {
setShowDrawer(true);
const data = await getListAll({ book_id: id });
let arr = convertToAntdTreeData(data, 'name');
setTreeData(arr);
};
const handleCloseDrawer = () => {
setShowDrawer(false);
};
const [classData, setClassData] = useState([]);
const [producersData, setproducersData] = useState([]);
const [pressData, setpressData] = useState([]);
// 出品方/出版社
const getProducers = async () => {
const producersList = await publisherList({ types: 1 });
const press = await publisherList({ types: 2 });
setproducersData(producersList);
setpressData(press);
};
const [FormLoad, setFormLoad] = useState(true);
const [checkedKeys, setcheckedKeys] = useState([]);
const submitTree = (obj) => {
form.setFieldValue('trials', checkedKeys);
handleCloseDrawer(false);
};
const ontreeCheck = (keys, { checked, checkedNodes, node, halfCheckedKeys }) => {
setcheckedKeys(keys);
};
const getinfo = async () => {
setFormLoad(true);
// 详情
const data = await getInfoById({ id });
const { category_id, producers_id, press_id } = data;
let obj = { category_id, producers_id, press_id };
setBookType(data.is_free);
for (let key in obj) {
if (obj[key] === 0) {
obj[key] = null;
}
}
form.setFieldsValue({ ...data, ...obj });
setFormLoad(false);
};
const getInfo = async () => {
// 分类
const { list } = await getList({ page: 1, page_size: 999 });
setClassData(list);
getProducers();
getinfo();
};
const onFinish = async (obj) => {
const bool = await editBook({ ...obj, id });
if (!bool) return;
navigate(-1);
};
useEffect(() => {
getInfo();
}, []);
return (
<Spin spinning={FormLoad} style={{ padding: 20 }}>
<Form labelCol={{ span: 2 }} form={form} initialValues={initialValues} onFinish={onFinish}>
<Form.Item label='书籍名称' name='name'>
<Input
disabled
allowClear
autoComplete='off'
placeholder='请输入书籍名称'
style={{ width: 400 }}
></Input>
</Form.Item>
<Form.Item label='书籍作者' name='authors'>
<Input autoComplete='off' disabled style={{ width: 260 }}></Input>
</Form.Item>
<Form.Item
label='分类'
name='category_id'
rules={[{ required: true, message: '请选择分类' }]}
>
<Select placeholder='请选择分类' style={{ width: 260 }}>
{classData &&
classData.map((item) => (
<Select.Option value={item.id} key={item.id}>
{item.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label='出品方' required style={producersConfig.style}>
<Space>
<Form.Item
noStyle
name='producers_id'
rules={[{ required: true, message: '请选择出品方' }]}
>
<Select placeholder='请选择出品方' style={{ width: 260 }}>
{producersData?.map((item) => (
<Select.Option key={item.id} value={item.id}>
{item.publisher_name}
</Select.Option>
))}
</Select>
</Form.Item>
<a
style={{
textDecoration: 'underline',
color: '#1672EC',
cursor: 'pointer',
}}
onClick={() => {
setproducersConfig({ ...producersConfig, style: { marginBottom: 0 } });
setShowAddCategory(true);
}}
>
添加出品方
</a>
</Space>
{showAddCategory && (
<div style={{ marginTop: '10px' }}>
<Space>
<Form.Item
noStyle={producersConfig.noStyle}
validateStatus={producersConfig.validateStatus}
help={producersConfig.help}
>
<Input
style={{ width: 260 }}
autoComplete='off'
placeholder='请输入新的出品方'
value={publisher_name}
allowClear
onChange={(e) => {
let val = e.target.value;
if (val) {
setproducersConfig({ ...producersConfig, help: '', validateStatus: '' });
}
setPublisherName(val);
}}
/>
</Form.Item>
<a
type='link'
style={{ textDecoration: 'underline', color: '#1672EC' }}
onClick={() =>
addproducers({ types: 1, publisher_name }, () => setShowAddCategory(false))
}
>
添加
</a>
<a
type='link'
style={{ textDecoration: 'underline', color: '#AA1941' }}
onClick={() => {
setproducersConfig({ validateStatus: '', help: '', noStyle: false, style: {} });
setShowAddCategory(false); // 可选:添加后关闭输入框
}}
>
删除
</a>
</Space>
</div>
)}
</Form.Item>
<Form.Item label='出版社' required style={pressConfig.style}>
<Space>
<Form.Item
noStyle
name='press_id'
rules={[{ required: true, message: '请选择出版社' }]}
>
<Select placeholder='请选择出品方' style={{ width: 260 }}>
{pressData?.map((item) => {
return (
<Select.Option key={item.id} value={item.id}>
{item.publisher_name}
</Select.Option>
);
})}
</Select>
</Form.Item>
<a
style={{
textDecoration: 'underline',
color: '#1672EC',
cursor: 'pointer',
}}
onClick={() => {
setpressConfig({ ...pressConfig, style: { marginBottom: 0 } });
setShowAddCategory1(true);
}}
>
添加出版社
</a>
</Space>
{showAddCategory1 && (
<div style={{ marginTop: '10px' }}>
<Space>
<Form.Item
noStyle={pressConfig.noStyle}
validateStatus={pressConfig.validateStatus}
help={pressConfig.help}
>
<Input
style={{ width: 260 }}
autoComplete='off'
placeholder='请输入新的出版社'
allowClear
value={publisher_name1}
onChange={(e) => {
let val = e.target.value;
if (val) {
setpressConfig({ ...pressConfig, help: '', validateStatus: '' });
}
setPublisherName1(e.target.value);
}}
/>
</Form.Item>
<a
type='link'
style={{ textDecoration: 'underline', color: '#1672EC' }}
onClick={() =>
addproducers({ types: 2, publisher_name: publisher_name1 }, () =>
setShowAddCategory1(false),
)
}
>
添加
</a>
<a
type='link'
style={{ textDecoration: 'underline', color: '#AA1941' }}
onClick={() => {
setpressConfig({ validateStatus: '', help: '', noStyle: false, style: {} });
setShowAddCategory1(false);
}}
>
删除
</a>
</Space>
</div>
)}
</Form.Item>
<Form.Item label='书籍类型' name='is_free'>
<Radio.Group
onChange={(e) => {
setBookType(e.target.value);
}}
>
<Radio value={1}>免费</Radio>
<Radio value={0}>付费</Radio>
</Radio.Group>
</Form.Item>
{bookType === 0 && (
<Form.Item label='试读章节' name='trials'>
<a type='link' style={{ textDecoration: 'underline' }} onClick={handleOpenDrawer}>
选择试读章节
</a>
</Form.Item>
)}
{bookType === 0 && (
<>
<Form.Item label='零售价(元):' name='price'>
<Input
value={price}
onChange={(e) => setprice(e.target.value)}
autoComplete='off'
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item label='折扣价(元):'>
<Form.Item name='vip_price' style={{ display: 'inline-block' }}>
<Input
value={vip_price}
onChange={(e) => setvip_price(e.target.value)}
autoComplete='off'
allowClear
style={{ width: 80 }}
></Input>
</Form.Item>
<Form.Item style={{ display: 'inline-block', margin: '0 8px' }}>
<Input
value={discountVal}
allowClear
onChange={handleDiscountRateChange}
onBlur={handleDiscountRateBlur} // 添加失去焦点事件处理函数
autoComplete='off'
style={{ width: 80 }}
></Input>
折
</Form.Item>
</Form.Item>
</>
)}
<Form.Item wrapperCol={{ offset: 2 }}>
<Space className='footer' size={20}>
<Button
onClick={() => {
navigate(-1);
}}
>
取消
</Button>
<Button type='primary' htmlType='submit'>
确认
</Button>
</Space>
</Form.Item>
</Form>
<Drawer
placement='right'
onClose={handleCloseDrawer}
open={showDrawer}
labelCol={{ span: 7 }}
mask={false}
>
<Form onFinish={submitTree}>
<Form.Item name='power_list' style={{ padding: 10 }}>
<Tree
checkable
checkedKeys={checkedKeys}
defaultExpandAll={false} //让授权后的弹窗只展示根标签
treeData={treeData}
// showLine //删除这里,树形结构左侧的下拉线消失,图标从+-更改为默认的△
// checkStrictly
onCheck={ontreeCheck}
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 10, span: 16 }}>
<Space size={20}>
<Button className='cancel' onClick={() => handleCloseDrawer(false)}>
取消
</Button>
<Button className='submit' htmlType='submit'>
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
</Spin>
);
};
export default SaleEdit;