ERPTurbo_Client/packages/app-client/src/components/purchase/section/PackagingCostSection.tsx
shenyifei 323fe4c83d feat(purchase): 新增草帘费用项功能并优化订单创建流程
- 在OrderVehicle模块中新增草帘费用项的添加与移除逻辑
- 根据选中状态动态更新orderCostList中的草帘费用项
- 优化PurchaseOrderWithdrawReview组件按钮点击事件处理
- 调整OrderPackage组件品牌选择过滤逻辑及数据结构
- 完善Weigh组件弹窗交互与样式布局
- 修复PackagingCostSection组件默认计提费用配置
- 升级delivery文档页面otherFees模块实时获取最新费用项目
- 优化delivery页面预览内容展示格式和数据填充逻辑
- 更新create页面传递orderCostList至子组件确保数据同步
- 引入generateShortId工具用于生成唯一订单费用ID
2025-11-17 18:55:39 +08:00

893 lines
31 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",
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: "",
payee: "",
quantity: "",
unitPrice: "",
amount: "",
}));
}}
>
{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={[
fixedCostOptions.filter((option) => {
// 检查该固定费用类型是否已存在于fixedCosts中
return !fixedCosts.some(
(cost) => cost.name === option.label,
);
}),
]}
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);
// 重置表单
setNewCostData({
payerType: "",
principal: "",
costType: "",
itemId: generateShortId(),
name: "",
payee: "",
quantity: "",
unitPrice: "",
amount: "",
});
}}
>
</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>
))}
</>
);
}