ERPTurbo_Client/packages/app-client/src/components/purchase/section/PackagingCostSection.tsx
shenyifei bd4723b6ed feat(purchase): 新增成本差异和辅料费用编辑功能
- 新增 CostDifferenceSection 组件,支持分成金额调整与利润计算
- 新增 MaterialCostSection 组件,支持辅料费用的数量与单价编辑
- 优化 BasicInfoSection,增加车次号和运费类型的输入控件
- 重构 CostSummarySection,使用表格展示成本汇总信息
- 移除 LaborInfoSection 中的调试日志
- 调整 MarketPriceSection,改进供应商报价展示样式
- 优化 PackageInfoSection,增强纸箱信息的可编辑交互
- 清理 DealerInfoSection 中切换经销商时的冗余字段重置逻辑
2025-11-12 18:40:45 +08:00

888 lines
30 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;
onChange?: (purchaseOrderVO: BusinessAPI.PurchaseOrderVO) => void;
readOnly?: boolean;
}) {
const { purchaseOrderVO, onChange, readOnly } = props;
// 弹窗相关状态
const [visiblePopup, setVisiblePopup] = useState<{ [key: string]: boolean }>(
{},
);
// 新增包装费弹窗状态
const [showAddCostPopup, setShowAddCostPopup] = useState(false);
// 新增包装费表单数据
const [newCostData, setNewCostData] = useState({
costType: "", // 费用类型
itemId: "", // 费用项目ID
name: "", // 费用名称
payee: "", // 收款人
quantity: "", // 数量(包数或人数)
unitPrice: "", // 单价
amount: "", // 金额(固定费用和其他费用使用)
payerType: "", // 费用承担方,默认我方
principal: "", // 工头姓名
});
// 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 fixedCostOptions = [
{ label: "计提费", value: "accrual" },
{ label: "王超费用", value: "wangchao" },
{ label: "收代办费", value: "agency" },
];
// 固定费用
const fixedCosts =
purchaseOrderVO.orderCostList?.filter(
(item) => item.costType === "FIXED_COST",
) || [];
// 其他费用
const otherCosts =
purchaseOrderVO.orderCostList?.filter(
(item) => item.costType === "OTHER_COST",
) || [];
// 检查是否存在计提费,如果不存在则添加一个默认的计提费
useEffect(() => {
if (onChange) {
// 检查是否已存在计提费
const hasAccrualFee = fixedCosts.some((item) => item.name === "计提费");
if (!hasAccrualFee) {
// 创建默认计提费
const defaultAccrualFee: BusinessAPI.OrderCost = {
itemId: generateShortId(),
orderCostId: generateShortId(),
name: "计提费",
costType: "FIXED_COST",
price: 0,
count: 1,
unit: "项",
payerType: "US",
};
// 更新purchaseOrderVO添加默认计提费
const updatedOrderCostList = [
...(purchaseOrderVO.orderCostList || []),
defaultAccrualFee,
];
const newPurchaseOrderVO = {
...purchaseOrderVO,
orderCostList: updatedOrderCostList,
};
onChange(newPurchaseOrderVO as any);
}
}
}, [purchaseOrderVO.orderCostList]);
// 当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="grid grid-cols-1 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 p-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 p-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>
);
})}
</View>
{!readOnly && (
<Button
type={"primary"}
block
size={"large"}
onClick={() => setShowAddCostPopup(true)}
>
</Button>
)}
{/* 新增其他费用*/}
<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="border-neutral-base relative flex h-10 w-full items-center rounded-md border border-solid"
onClick={() =>
setPickerVisible((prev) => ({ ...prev, costType: true }))
}
>
<Input
type="text"
placeholder="请选择费用类型"
value={
costTypeOptions.find(
(option) => option.value === newCostData.costType,
)?.label || ""
}
disabled
/>
<Icon name="chevron-down" className="absolute -right-1 mr-4" />
</View>
<Picker
title="请选择费用类型"
visible={pickerVisible.costType}
options={[costTypeOptions]}
onConfirm={(_, values) => {
const selectedValue = values[0] as string;
setNewCostData((prev) => ({
...prev,
costType: selectedValue,
itemId: "",
name: "",
payee: "",
quantity: "",
unitPrice: "",
amount: "",
}));
setPickerVisible((prev) => ({ ...prev, costType: false }));
}}
onCancel={() =>
setPickerVisible((prev) => ({ ...prev, costType: false }))
}
onClose={() =>
setPickerVisible((prev) => ({ ...prev, costType: false }))
}
/>
{/* 固定费用子类型 */}
{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={[fixedCostOptions]}
onConfirm={(_, values) => {
const selectedValue = values[0] as string;
const selectedItem = fixedCostOptions.find(
(option) => option.value === selectedValue,
);
if (selectedItem) {
setNewCostData((prev) => ({
...prev,
name: selectedItem.label,
}));
}
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.payee}
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)}
>
</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.payee)
) {
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({
payerType: "",
principal: "",
costType: "",
itemId: generateShortId(),
name: "",
payee: "",
quantity: "",
unitPrice: "",
amount: "",
});
}}
>
</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>
))}
</>
);
}