ERPTurbo_Client/packages/app-client/src/components/purchase/section/PackagingCostSection.tsx
shenyifei a69525bfb5 feat(purchase): 实现空箱费用计算和空箱信息展示功能
- 在生产环境配置中添加海报域名环境变量
- 优化经销商选择器组件样式和交互
- 引入 decimal.js 库用于精确计算空箱费用
- 实现空箱费用自动计算和固定费用项生成功能
- 添加空箱使用明细展示和分组统计功能
- 完善空箱信息编辑功能,支持销售价、成本价、箱重编辑
- 优化订单预览页面空箱信息展示逻辑
- 移除重复的费用项目获取逻辑,统一通过 props 传递
- 修复纸箱品牌过滤条件错误问题
- 优化输入框和选择器组件样式和交互体验
2025-11-18 15:16:30 +08:00

913 lines
32 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 { ScrollView, View } from "@tarojs/components";
import {
Button,
Input,
Picker,
Popup,
SafeArea,
} from "@nutui/nutui-react-taro";
import { useEffect, useState } from "react";
import { Icon } from "@/components";
import { formatCurrency, validatePrice } from "@/utils/format";
import { generateShortId } from "@/utils/generateShortId";
export default function PackagingCostSection(props: {
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
costItemVOList: BusinessAPI.CostItemVO[];
onChange?: (purchaseOrderVO: BusinessAPI.PurchaseOrderVO) => void;
readOnly?: boolean;
}) {
const { purchaseOrderVO, onChange, readOnly, costItemVOList } = props;
// 弹窗相关状态
const [visiblePopup, setVisiblePopup] = useState<{ [key: string]: boolean }>(
{},
);
// 新增包装费弹窗状态
const [showAddCostPopup, setShowAddCostPopup] = useState(false);
// 新增包装费表单数据
const [newCostData, setNewCostData] = useState({
costType: "FIXED_COST", // 费用类型
itemId: "", // 费用项目ID
name: "", // 费用名称
quantity: 0, // 数量(人数)
unit: "", // 单位
unitPrice: "", // 单价
amount: "", // 金额
payerType: "US", // 费用承担方,默认我方
principal: "", // 工头姓名
requireQuantityAndPrice: false,
});
// Picker可见状态
const [pickerVisible, setPickerVisible] = useState({
costType: false, // 费用类型Picker
costItem: false, // 费用项目Picker
fixedCost: false, // 固定费用Picker
});
// 编辑值的状态
const [editValues, setEditValues] = useState<{
[key: string]: {
count: number;
price: number;
name?: string;
principal?: string;
payerType?: "US" | "OTHER";
};
}>({});
// 临时编辑值的状态(用于在保存前暂存编辑的值)
const [tempEditValues, setTempEditValues] = useState<{
[key: string]: {
count: number;
price: number;
name?: string;
principal?: string;
payerType?: "US" | "OTHER";
};
}>({});
// 初始化编辑值
const initEditValues = (
itemId: string,
count: number,
price: number,
name?: string,
principal?: string,
payerType?: "US" | "OTHER",
) => {
if (!editValues[itemId]) {
setEditValues((prev) => ({
...prev,
[itemId]: { count, price, name, principal, payerType },
}));
}
// 同时初始化临时编辑值
if (!tempEditValues[itemId]) {
setTempEditValues((prev) => ({
...prev,
[itemId]: { count, price, name, principal, payerType },
}));
}
};
// 费用类型选项
const costTypeOptions = [
{ label: "固定费用", value: "FIXED_COST" },
{ label: "其他费用", value: "OTHER_COST" },
];
// 固定费用
const fixedCosts =
purchaseOrderVO.orderCostList?.filter(
(item) => item.costType === "FIXED_COST",
) || [];
// 其他费用
const otherCosts =
purchaseOrderVO.orderCostList?.filter(
(item) => item.costType === "OTHER_COST",
) || [];
// 检查是否存在计提费,如果不存在则添加一个默认的计提费
useEffect(() => {
if (onChange) {
// 检查是否已存在计提费
const accrualFeeItem = costItemVOList.find(
(item) => item.name === "计提费" && item.costType === "FIXED_COST",
);
const hasAccrualFee = fixedCosts.some((item) => item.name === "计提费");
if (!hasAccrualFee && accrualFeeItem) {
// 创建默认计提费
const defaultAccrualFee: BusinessAPI.OrderCost = {
itemId: accrualFeeItem.itemId,
orderCostId: generateShortId(),
name: "计提费",
costType: "FIXED_COST",
price: 0,
count: 1,
unit: "项",
payerType: "US",
requireQuantityAndPrice: false,
};
// 更新purchaseOrderVO添加默认计提费
const updatedOrderCostList = [
...(purchaseOrderVO.orderCostList || []),
defaultAccrualFee,
];
const newPurchaseOrderVO = {
...purchaseOrderVO,
orderCostList: updatedOrderCostList,
};
onChange(newPurchaseOrderVO as any);
}
}
}, [purchaseOrderVO.orderCostList, onChange, fixedCosts]);
// 当editValues发生变化时更新purchaseOrderVO
useEffect(() => {
// 只有当onChange存在时才更新
if (onChange) {
// 获取现有的orderCostList
const existingOrderCostList = purchaseOrderVO.orderCostList || [];
// 更新已有的成本项
const updatedOrderCostList = existingOrderCostList.map((item) => {
if (editValues[item.orderCostId]) {
return {
...item,
price:
editValues[item.orderCostId].price !== undefined
? editValues[item.orderCostId].price
: item.price,
count:
editValues[item.orderCostId].count !== undefined
? editValues[item.orderCostId].count
: item.count,
name:
editValues[item.orderCostId].name !== undefined
? editValues[item.orderCostId].name
: item.name,
principal:
editValues[item.orderCostId].principal !== undefined
? editValues[item.orderCostId].principal
: item.principal,
payerType:
editValues[item.orderCostId].payerType !== undefined
? editValues[item.orderCostId].payerType
: item.payerType,
};
}
// 未修改的成本项保持原样
return item;
});
// 创建新的purchaseOrderVO对象
const newPurchaseOrderVO = {
...purchaseOrderVO,
orderCostList: updatedOrderCostList,
};
// 调用onChange回调
onChange(newPurchaseOrderVO as any);
}
}, [editValues]);
return (
<>
<View className="flex flex-col gap-2.5">
{/* 固定费用 */}
{fixedCosts.map((item, index) => {
// 初始化编辑值
initEditValues(item.orderCostId, item.count, item.price);
return (
<View
className="flex items-center justify-between gap-2.5"
key={item.orderCostId}
onClick={() =>
setVisiblePopup((prev) => ({
...prev,
[item.orderCostId]: true,
}))
}
>
<View className="text-neutral-dark flex-shrink-0 text-sm">
{index + 1}-{item.name}
</View>
<View className="text-neutral-darkest flex flex-row items-center justify-between gap-2.5 text-sm font-medium">
{formatCurrency(
Number(
(editValues[item.orderCostId]?.price || item.price) *
(editValues[item.orderCostId]?.count || item.count),
),
)}
{!readOnly && (
<View className="ml-1 text-gray-500">
<Icon name={"pen-to-square"} size={16} color={"#1a73e8"} />
</View>
)}
</View>
</View>
);
})}
{/* 其他费用 */}
{otherCosts.map((item, index) => {
// 初始化编辑值
initEditValues(
item.orderCostId,
item.count,
item.price,
item.name,
item.principal,
);
return (
<View
className="flex items-center justify-between gap-2.5"
key={item.orderCostId}
onClick={() =>
setVisiblePopup((prev) => ({
...prev,
[item.orderCostId]: true,
}))
}
>
<View className="text-neutral-dark flex-shrink-0 text-sm">
{fixedCosts.length + index + 1}-
{editValues[item.orderCostId]?.name || item.name}
</View>
<View className="text-neutral-darkest flex flex-row items-center justify-between gap-2.5 text-sm font-medium">
{formatCurrency(
Number(
(editValues[item.orderCostId]?.price || item.price) *
(editValues[item.orderCostId]?.count || item.count),
),
)}
{!readOnly && (
<View className="ml-1 text-gray-500">
<Icon name={"pen-to-square"} size={16} color={"#1a73e8"} />
</View>
)}
</View>
</View>
);
})}
{!readOnly && (
<Button
type={"primary"}
block
size={"large"}
onClick={() => setShowAddCostPopup(true)}
>
</Button>
)}
</View>
{/* 新增其他费用*/}
<Popup
visible={showAddCostPopup}
position="bottom"
title="新增其他费用"
onClose={() => setShowAddCostPopup(false)}
onOverlayClick={() => setShowAddCostPopup(false)}
lockScroll
>
<ScrollView scrollY className="max-h-150">
<View className="flex flex-col gap-3 p-2.5">
{/* 费用类型选择 */}
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View className="flex flex-row gap-4">
{costTypeOptions.map((option) => (
<View
key={option.value}
className={`flex flex-1 flex-row items-center justify-center rounded-md py-2 ${
newCostData.costType === option.value
? "bg-blue-100 text-blue-600"
: "border border-gray-300"
}`}
onClick={() => {
setNewCostData((prev) => ({
...prev,
costType: option.value,
itemId: "",
name: "", // 费用名称
quantity: 0, // 数量
unit: "", // 单位
unitPrice: "", // 单价
amount: "", // 金额
requireQuantityAndPrice: false,
}));
}}
>
{option.label}
</View>
))}
</View>
{/* 固定费用子类型 */}
{newCostData.costType === "FIXED_COST" && (
<>
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View
className="border-neutral-base relative flex h-10 w-full items-center rounded-md border border-solid"
onClick={() =>
setPickerVisible((prev) => ({ ...prev, fixedCost: true }))
}
>
<Input
type="text"
placeholder="请选择固定费用类型"
value={newCostData.name || ""}
disabled
/>
<Icon
name="chevron-down"
className="absolute -right-1 mr-4"
/>
</View>
<Picker
title="请选择垫付项目"
visible={pickerVisible.fixedCost}
options={[
costItemVOList
.filter((item) => item.costType === newCostData.costType)
.filter((item) => {
// 检查该项目是否已经被选择
return !fixedCosts.some(
(hc) => hc.itemId === item.itemId,
);
})
.map((item) => ({
label: item.name,
value: item.itemId,
})),
]}
onConfirm={(_, values) => {
const selectedValue = values[0] as string;
const selectedItem = costItemVOList.find(
(item) => item.itemId === selectedValue,
);
if (selectedItem) {
setNewCostData((prev) => ({
...prev,
itemId: selectedValue,
name: selectedItem.name,
payerType: "US",
quantity: selectedItem.requireQuantityAndPrice ? 0 : 1,
unit: selectedItem.requireQuantityAndPrice
? selectedItem.unit
: "元", // 单位
unitPrice: "", // 单价
amount: "", // 金额
principal: "", // 工头姓名
requireQuantityAndPrice:
selectedItem.requireQuantityAndPrice,
}));
}
setPickerVisible((prev) => ({ ...prev, fixedCost: false }));
}}
onCancel={() =>
setPickerVisible((prev) => ({ ...prev, fixedCost: false }))
}
onClose={() =>
setPickerVisible((prev) => ({ ...prev, fixedCost: false }))
}
/>
</>
)}
{/* 其他费用名称输入 */}
{newCostData.costType === "OTHER_COST" && (
<>
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View className="border-neutral-base flex flex-row items-center rounded-md border border-solid">
<Input
className="placeholder:text-neutral-dark flex-1"
placeholder="请输入费用名称"
value={newCostData.name}
onChange={(value) => {
setNewCostData((prev) => ({
...prev,
name: value,
}));
}}
/>
</View>
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View className="border-neutral-base flex flex-row items-center rounded-md border border-solid">
<Input
className="placeholder:text-neutral-dark flex-1"
placeholder="请输入收款人姓名"
value={newCostData.principal}
onChange={(value) => {
setNewCostData((prev) => ({
...prev,
payee: value,
}));
}}
/>
</View>
</>
)}
{/* 金额输入(固定费用和其他费用)*/}
{((newCostData.costType === "FIXED_COST" && newCostData.name) ||
newCostData.costType === "OTHER_COST") && (
<>
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View className="border-neutral-base flex flex-row items-center rounded-md border border-solid">
<Input
className="placeholder:text-neutral-dark flex-1"
placeholder="请输入金额"
type="digit"
value={newCostData.amount?.toString() || ""}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setNewCostData((prev) => ({
...prev,
amount: numValue as any,
}));
}
}}
/>
<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={() => {
setShowAddCostPopup(false);
// 重置表单
setNewCostData({
itemId: generateShortId(),
costType: "",
payerType: "",
principal: "",
name: "", // 费用名称
quantity: 0, // 数量
unit: "", // 单位
unitPrice: "", // 单价
amount: "", // 金额
requireQuantityAndPrice: false,
});
}}
>
</Button>
</View>
<View className="flex-1">
<Button
size="large"
block
type="primary"
onClick={() => {
// 检查必填项
if (!newCostData.costType) {
console.log("请选择费用类型");
return;
}
if (
newCostData.costType === "OTHER_COST" &&
(!newCostData.name || !newCostData.principal)
) {
console.log("请填写费用名称和收款人姓名");
return;
}
// 检查固定费用和其他费用的金额
if (
(newCostData.costType === "FIXED_COST" ||
newCostData.costType === "OTHER_COST") &&
!newCostData.amount
) {
console.log("请输入金额");
return;
}
// 创建新的费用项
const newOrderCost: BusinessAPI.OrderCost = {
orderCostId: generateShortId(),
itemId: newCostData.itemId || "",
name: newCostData.name,
price: Number(
newCostData.unitPrice || newCostData.amount || 0,
),
unit:
newCostData.costType === "PACKAGING_MATERIALS"
? "包"
: newCostData.costType === "HUMAN_COST"
? "人"
: newCostData.costType === "FIXED_COST" ||
newCostData.costType === "OTHER_COST"
? "项"
: "",
count: Number(newCostData.quantity || 1),
//@ts-ignore
payerType: newCostData.payerType || "US",
costType:
newCostData.costType === "PACKAGING_MATERIALS"
? "PACKAGING_MATERIALS"
: newCostData.costType === "HUMAN_COST"
? "HUMAN_COST"
: newCostData.costType === "FIXED_COST"
? "FIXED_COST"
: "OTHER_COST",
principal:
newCostData.payerType === "OTHER"
? ""
: newCostData.principal || "",
};
// 更新purchaseOrderVO将新费用添加到orderCostList中
if (onChange) {
const updatedOrderCostList = [
...(purchaseOrderVO.orderCostList || []),
newOrderCost,
];
const newPurchaseOrderVO = {
...purchaseOrderVO,
orderCostList: updatedOrderCostList,
};
onChange(newPurchaseOrderVO);
}
setShowAddCostPopup(false);
// 重置表单
setNewCostData({
itemId: generateShortId(),
costType: "",
payerType: "",
principal: "",
name: "", // 费用名称
quantity: 0, // 数量
unit: "", // 单位
unitPrice: "", // 单价
amount: "", // 金额
requireQuantityAndPrice: false,
});
}}
>
</Button>
</View>
</View>
</View>
<SafeArea position="bottom" />
</ScrollView>
</Popup>
{/* 固定费用编辑弹窗 */}
{fixedCosts.map((item) => (
<Popup
key={item.orderCostId}
visible={visiblePopup[item.orderCostId]}
position="bottom"
title={`编辑${item.name}`}
onClose={() =>
setVisiblePopup((prev) => ({ ...prev, [item.orderCostId]: false }))
}
onOverlayClick={() =>
setVisiblePopup((prev) => ({ ...prev, [item.orderCostId]: false }))
}
lockScroll
>
<View className="flex flex-col gap-3 p-2.5">
<View className="text-neutral-darkest text-sm font-medium">
</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[item.orderCostId]?.price?.toString() || ""
}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setTempEditValues((prev) => ({
...prev,
[item.orderCostId]: {
...prev[item.orderCostId],
price: numValue as number,
count: 1,
},
}));
}
}}
/>
<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,
[item.orderCostId]: false,
}))
}
>
</Button>
</View>
<View className="flex-1">
<Button
size="large"
block
type="primary"
onClick={() => {
// 保存时才更新editValues状态
setEditValues((prev) => ({
...prev,
[item.orderCostId]: {
...tempEditValues[item.orderCostId],
},
}));
// 这里应该调用更新接口或更新状态
setVisiblePopup((prev) => ({
...prev,
[item.orderCostId]: false,
}));
}}
>
</Button>
</View>
<View className="flex-1">
<Button
size="large"
block
type="danger"
onClick={() => {
// 删除费用项
setEditValues((prev) => {
const newEditValues = { ...prev };
delete newEditValues[item.orderCostId];
return newEditValues;
});
// 更新purchaseOrderVO移除被删除的费用项
if (onChange) {
const newOrderCostList = (
purchaseOrderVO.orderCostList || []
).filter((cost) => cost.orderCostId !== item.orderCostId);
const newPurchaseOrderVO = {
...purchaseOrderVO,
orderCostList: newOrderCostList,
};
onChange(newPurchaseOrderVO);
}
// 关闭弹窗
setVisiblePopup((prev) => ({
...prev,
[item.orderCostId]: false,
}));
}}
>
</Button>
</View>
</View>
</View>
<SafeArea position="bottom" />
</Popup>
))}
{/* 其他费用编辑弹窗 */}
{otherCosts.map((item) => (
<Popup
key={item.orderCostId}
visible={visiblePopup[item.orderCostId]}
position="bottom"
title={`编辑${editValues[item.orderCostId]?.name || item.name}`}
onClose={() =>
setVisiblePopup((prev) => ({ ...prev, [item.orderCostId]: false }))
}
onOverlayClick={() =>
setVisiblePopup((prev) => ({ ...prev, [item.orderCostId]: false }))
}
lockScroll
>
<ScrollView scrollY className="max-h-150">
<View className="flex flex-col gap-3 p-2.5">
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View className="border-neutral-base flex flex-row items-center rounded-md border border-solid">
<Input
className="placeholder:text-neutral-dark flex-1"
placeholder="请输入费用名称"
value={tempEditValues[item.orderCostId]?.name || item.name}
onChange={(value) => {
setTempEditValues((prev) => ({
...prev,
[item.orderCostId]: {
...prev[item.orderCostId],
name: value,
},
}));
}}
/>
</View>
<View className="text-neutral-darkest text-sm font-medium">
</View>
<View className="border-neutral-base flex flex-row items-center rounded-md border border-solid">
<Input
className="placeholder:text-neutral-dark flex-1"
placeholder="请输入收款人姓名"
value={
tempEditValues[item.orderCostId]?.principal ||
item.principal ||
""
}
onChange={(value) => {
setTempEditValues((prev) => ({
...prev,
[item.orderCostId]: {
...prev[item.orderCostId],
principal: value,
},
}));
}}
/>
</View>
<View className="text-neutral-darkest text-sm font-medium">
</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[item.orderCostId]?.price?.toString() || ""
}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setTempEditValues((prev) => ({
...prev,
[item.orderCostId]: {
...prev[item.orderCostId],
price: numValue as number,
count: 1,
},
}));
}
}}
/>
<View className="mr-2"></View>
</View>
</View>
</ScrollView>
<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,
[item.orderCostId]: false,
}))
}
>
</Button>
</View>
<View className="flex-1">
<Button
size="large"
block
type="primary"
onClick={() => {
// 保存时才更新editValues状态
setEditValues((prev) => ({
...prev,
[item.orderCostId]: {
...tempEditValues[item.orderCostId],
},
}));
// 这这里应该调用更新接口或更新状态
setVisiblePopup((prev) => ({
...prev,
[item.orderCostId]: false,
}));
}}
>
</Button>
</View>
<View className="flex-1">
<Button
size="large"
block
type="danger"
onClick={() => {
// 删除费用项
setEditValues((prev) => {
const newEditValues = { ...prev };
delete newEditValues[item.orderCostId];
return newEditValues;
});
// 更新purchaseOrderVO移除被删除的费用项
if (onChange) {
const newOrderCostList = (
purchaseOrderVO.orderCostList || []
).filter((cost) => cost.orderCostId !== item.orderCostId);
const newPurchaseOrderVO = {
...purchaseOrderVO,
orderCostList: newOrderCostList,
};
onChange(newPurchaseOrderVO);
}
// 关闭弹窗
setVisiblePopup((prev) => ({
...prev,
[item.orderCostId]: false,
}));
}}
>
</Button>
</View>
</View>
</View>
<SafeArea position="bottom" />
</Popup>
))}
</>
);
}