ERPTurbo_Client/packages/app-client/src/components/purchase/section/EmptyBoxInfoSection.tsx
shenyifei 3d217b1122 feat(purchase): 优化采购订单计算逻辑和界面展示
- 引入decimal.js提升金额计算精度
- 重构成本计算方法,明确区分各类费用构成
- 优化采购预览界面,增加计算明细展示
- 改进开票信息展示样式和计算公式说明
- 完善纸箱重量和销售金额的精确计算
- 调整界面布局,提升用户体验和信息可读性
- 修复成本项过滤逻辑,确保数据准确性
- 新增快速导航功能,便于页面内快速定位
- 更新图标资源,支持计算器和指南针图标
- 优化数字格式化处理,统一保留合适的小数位数
2025-12-13 11:08:25 +08:00

486 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { formatCurrency, validatePrice } from "@/utils";
import { useEffect, useState } from "react";
import { Button, Input, Popup, SafeArea, Table } from "@nutui/nutui-react-taro";
import { Icon } from "@/components";
import { View } from "@tarojs/components";
import { Decimal } from "decimal.js";
export default function EmptyBoxInfoSection(props: {
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
onChange?: (purchaseOrderVO: BusinessAPI.PurchaseOrderVO) => void;
readOnly?: boolean;
}) {
const { purchaseOrderVO, onChange, readOnly } = props;
const defaultColumns = [
{
title: "纸箱型号",
key: "boxProductName",
fixed: "left",
},
{
title: "个数",
key: "boxProductCount",
},
{
title: "销售单价(元/斤)",
key: "boxSalePrice",
render: (
value: BusinessAPI.OrderPackage & {
orderPackageId?: string;
isTotalRow?: boolean;
},
) => {
// 合计行不显示编辑按钮
if (value.isTotalRow) {
return formatCurrency(value.boxSalePrice as number);
}
return (
<View
className="flex w-full items-center justify-between"
onClick={(e) => {
if (!readOnly) {
e.stopPropagation();
// 设置临时编辑值为当前值
setTempEditValues((prev) => ({
...prev,
[value.orderPackageId || ""]:
editValues[value.orderPackageId || ""],
}));
setVisiblePopup((prev) => ({
...prev,
[value.orderPackageId || ""]: true,
}));
}
}}
>
<View className={!readOnly ? "cursor-pointer underline" : ""}>
{formatCurrency(value.boxSalePrice as number)}
</View>
{!readOnly && (
<View className="-m-2 ml-2 flex items-center justify-center p-2">
<Icon name={"pen-to-square"} size={16} color={"#1a73e8"} />
</View>
)}
</View>
);
},
},
{
title: "销售金额(元)",
key: "boxSalePayment",
render: (
value: BusinessAPI.OrderPackage & {
boxProductCount: number;
isTotalRow?: boolean;
},
) =>
formatCurrency(
Number((value?.boxSalePrice || 0) * value.boxProductCount) as number,
),
},
{
title: "箱重(斤)",
key: "boxProductWeight",
render: (
value: BusinessAPI.OrderPackage & {
orderPackageId?: string;
isTotalRow?: boolean;
},
) => formatCurrency(value.boxProductWeight),
},
{
title: "品牌",
key: "boxBrandName",
},
];
const [columns, setColumns] = useState<any[]>(defaultColumns);
useEffect(() => {
if (!readOnly) {
setColumns([...defaultColumns]);
}
}, [readOnly]);
// 弹窗可见状态
const [visiblePopup, setVisiblePopup] = useState<{ [key: string]: boolean }>(
{},
);
// 编辑值的状态
const [editValues, setEditValues] = useState<{
[key: string]: {
boxSalePrice?: number;
};
}>({});
// 临时编辑值的状态(用于在保存前暂存编辑的值)
const [tempEditValues, setTempEditValues] = useState<{
[key: string]: {
boxSalePrice?: number;
};
}>({});
// 初始化编辑值
const initEditValues = (pkgId: string, boxSalePrice?: number) => {
const updates: {
editValuesUpdate?: {
boxSalePrice?: number;
};
tempEditValuesUpdate?: {
boxSalePrice?: number;
};
} = {};
if (!editValues[pkgId]) {
updates.editValuesUpdate = {
boxSalePrice,
};
}
// 同时初始化临时编辑值
if (!tempEditValues[pkgId]) {
updates.tempEditValuesUpdate = {
boxSalePrice,
};
}
return updates;
};
// 将所有包装信息合并并按品牌、型号、规格分组
const groupedPackageData = purchaseOrderVO.orderPackageList?.reduce(
(acc, orderPackage) => {
// 生成分组键
const key = `${orderPackage.boxBrandName}-${orderPackage.boxProductName}-${orderPackage.boxSpecId}`;
// 转换规格字段
const boxSpecId = orderPackage.boxSpecId;
const boxSpecName = orderPackage.boxSpecName;
if (!acc[key]) {
acc[key] = {
...orderPackage,
boxSpecId,
boxSpecName,
boxProductCount: 0,
};
}
// 累加数量
acc[key].boxProductCount += orderPackage.boxCount || 0;
return acc;
},
{} as Record<string, any>,
);
// 转换为数组格式
const packageData = Object.values(groupedPackageData || {});
// 计算合计数据
const calculateTotals = () => {
if (!packageData || packageData.length === 0) {
return {};
}
// 计算各项合计
let totalBoxProductCount = 0;
let totalBoxSalePayment = 0;
let totalBoxProductWeight = 0;
packageData.forEach((pkg: any) => {
totalBoxProductCount += pkg.boxProductCount || 0;
totalBoxSalePayment +=
Number((pkg?.boxSalePrice || 0) * pkg.boxProductCount) || 0;
totalBoxProductWeight +=
Number((pkg?.boxProductWeight || 0) * pkg.boxProductCount) || 0;
});
return {
boxProductName: "合计",
boxProductCount: totalBoxProductCount,
boxSalePayment: totalBoxSalePayment,
boxProductWeight: totalBoxProductWeight,
isTotalRow: true, // 标记这是合计行
};
};
const totalsData = calculateTotals();
// 初始化所有包装项的编辑值
useEffect(() => {
const newEditValues = { ...editValues };
const newTempEditValues = { ...tempEditValues };
let hasEditValuesChanged = false;
let hasTempEditValuesChanged = false;
packageData.forEach((pkg: BusinessAPI.OrderPackage) => {
const pkgId = pkg.orderPackageId || "";
const updates = initEditValues(pkgId, pkg.boxSalePrice);
if (updates.editValuesUpdate) {
newEditValues[pkgId] = updates.editValuesUpdate;
hasEditValuesChanged = true;
}
if (updates.tempEditValuesUpdate) {
newTempEditValues[pkgId] = updates.tempEditValuesUpdate;
hasTempEditValuesChanged = true;
}
});
if (hasEditValuesChanged) {
setEditValues(newEditValues);
}
if (hasTempEditValuesChanged) {
setTempEditValues(newTempEditValues);
}
}, [packageData]);
// 当editValues发生变化时更新purchaseOrderVO
useEffect(() => {
// 只有当onChange存在时才更新
if (onChange) {
// 创建新的purchaseOrderVO对象
const newPurchaseOrderVO = { ...purchaseOrderVO };
// 更新供应商列表中的包装信息
if (newPurchaseOrderVO.orderPackageList) {
newPurchaseOrderVO.orderPackageList =
newPurchaseOrderVO.orderPackageList.map((pkg) => {
const editValue = editValues[pkg.orderPackageId || ""];
if (editValue) {
return {
...pkg,
boxSalePrice:
editValue.boxSalePrice !== undefined
? editValue.boxSalePrice
: pkg.boxSalePrice,
};
}
return pkg;
});
}
// 调用onChange回调
onChange(newPurchaseOrderVO);
}
}, [editValues]);
// 自定义合计行渲染
const renderTableWithTotals = () => {
// 创建包含合计行的数据
const dataWithTotals = [...packageData];
if (Object.keys(totalsData).length > 0) {
dataWithTotals.push(totalsData);
}
// 自定义列配置,对合计行特殊处理
const columnsWithTotalsRender = columns.map((column) => {
if (column.key === "boxProductName") {
// 品牌列显示"合计"
return {
...column,
render: (rowData: any) => {
if (rowData.isTotalRow) {
return (
<span style={{ fontWeight: "bold" }}>
{rowData.boxProductName}
</span>
);
}
return rowData.boxProductName;
},
};
} else if (column.key === "boxProductCount") {
// 个数列显示合计
return {
...column,
render: (rowData: any) => {
if (rowData.isTotalRow) {
return (
<span style={{ fontWeight: "bold" }}>
{rowData.boxProductCount}
</span>
);
}
return rowData.boxProductCount;
},
};
} else if (column.key === "boxSalePrice") {
// 销售单价列合计行处理
return {
...column,
render: (rowData: any) => {
if (rowData.isTotalRow) {
return (
<span style={{ fontWeight: "bold" }}>
{formatCurrency(rowData.boxSalePrice as number)}
</span>
);
}
return column.render(rowData, rowData);
},
};
} else if (column.key === "boxSalePayment") {
// 销售金额列显示合计
return {
...column,
render: (rowData: any) => {
if (rowData.isTotalRow) {
return (
<span style={{ fontWeight: "bold" }}>
{formatCurrency(rowData.boxSalePayment)}
</span>
);
}
return new Decimal(rowData?.boxSalePrice || 0)
.mul(rowData.boxProductCount)
.toDecimalPlaces(0, Decimal.ROUND_HALF_UP)
.toNumber();
},
};
} else if (column.key === "boxProductWeight") {
// 重量列合计行处理
return {
...column,
render: (rowData: any) => {
if (rowData.isTotalRow) {
return (
<span style={{ fontWeight: "bold" }}>
{formatCurrency(rowData.boxProductWeight)}
</span>
);
}
return column.render(rowData, rowData);
},
};
}
// 其他列保持原有render函数或者默认显示
return column;
});
return (
<Table
className={"table-sum"}
columns={columnsWithTotalsRender}
data={dataWithTotals}
striped
/>
);
};
return (
<>
{renderTableWithTotals()}
{/* 纸箱价格编辑弹窗 */}
{packageData.map((pkg: BusinessAPI.OrderPackage) => (
<Popup
duration={150}
style={{
minHeight: "auto",
}}
key={pkg.orderPackageId}
visible={visiblePopup[pkg.orderPackageId || ""]}
position="bottom"
title="编辑纸箱信息"
onClose={() =>
setVisiblePopup((prev) => ({
...prev,
[pkg.orderPackageId || ""]: false,
}))
}
onOverlayClick={() =>
setVisiblePopup((prev) => ({
...prev,
[pkg.orderPackageId || ""]: false,
}))
}
lockScroll
>
<View className="flex flex-col gap-3 p-2.5">
<View className="text-neutral-darkest flex flex-row text-sm font-medium">
<Icon name="money-bill" size={16} className="mr-1" />
</View>
<View className="border-neutral-base flex flex-row items-center rounded-md border border-solid">
<Input
className="placeholder:text-neutral-dark"
placeholder="请输入销售单价"
type="digit"
value={
tempEditValues[
pkg.orderPackageId || ""
]?.boxSalePrice?.toString() || ""
}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setTempEditValues((prev) => ({
...prev,
[pkg.orderPackageId || ""]: {
...prev[pkg.orderPackageId || ""],
boxSalePrice: numValue as number,
},
}));
}
}}
/>
<View className="mr-2"></View>
</View>
</View>
<View className="flex w-full flex-col bg-white">
<View className="flex flex-row gap-2 p-3">
<View className="flex-1">
<Button
size="large"
block
type="default"
onClick={() =>
setVisiblePopup((prev) => ({
...prev,
[pkg.orderPackageId || ""]: false,
}))
}
>
</Button>
</View>
<View className="flex-1">
<Button
size="large"
block
type="primary"
onClick={() => {
// 保存时才更新editValues状态
setEditValues((prev) => ({
...prev,
[pkg.orderPackageId || ""]: {
...tempEditValues[pkg.orderPackageId || ""],
},
}));
// 关闭弹窗
setVisiblePopup((prev) => ({
...prev,
[pkg.orderPackageId || ""]: false,
}));
}}
>
</Button>
</View>
</View>
</View>
<SafeArea position="bottom" />
</Popup>
))}
</>
);
}