- 移除调试日志输出 - 新增 requireQuantityAndPrice 字段支持 - 调整页面样式间距统一为 p-2.5 - 交换工头垫付与产地垫付显示逻辑 - 完善费用弹窗编辑功能,支持数量单价分别输入 - 增加新手引导 tour 功能 - 优化订单成本列表更新逻辑,避免重复项 - 导出新增的费用 section 组件 - 替换 LaborInfoSection为 WorkerAdvanceSection - 引入 ProductionAdvanceSection 组件
511 lines
17 KiB
TypeScript
511 lines
17 KiB
TypeScript
import { View } from "@tarojs/components";
|
||
import { Icon } from "@/components";
|
||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||
import { business } from "@/services";
|
||
import { Button, Checkbox, Input, Toast } from "@nutui/nutui-react-taro";
|
||
import { CostItem, SupplierVO } from "@/types/typings";
|
||
import { generateShortId } from "@/utils/generateShortId";
|
||
|
||
// 定义ref暴露的方法接口
|
||
export interface OrderCostRef {
|
||
validate: () => boolean;
|
||
}
|
||
|
||
export interface IOrderCostProps {
|
||
value: CostItem[];
|
||
supplierVO: SupplierVO;
|
||
onChange?: (costItemList: CostItem[]) => void;
|
||
onAdd: () => void;
|
||
}
|
||
|
||
export default forwardRef<OrderCostRef, IOrderCostProps>(
|
||
function OrderCost(IOrderCostProps, ref) {
|
||
const { value, supplierVO, onChange, onAdd } = IOrderCostProps;
|
||
|
||
const [costItemVOList, setCostItemVOList] = useState<CostItem[]>();
|
||
|
||
// 总的工头姓名状态
|
||
const [principal, setPrincipal] = useState<string>("");
|
||
// 工头姓名错误状态
|
||
const [principalError, setPrincipalError] = useState<boolean>(false);
|
||
|
||
const init = async () => {
|
||
const { data } = await business.costItem.listCostItem({
|
||
costItemListQry: {
|
||
status: true,
|
||
showInEntry: true,
|
||
},
|
||
});
|
||
const costItemList = data.data;
|
||
|
||
if (costItemList) {
|
||
// 人工辅料选中
|
||
const initialList =
|
||
costItemList
|
||
?.filter(
|
||
(item) =>
|
||
item.costType === "HUMAN_COST" ||
|
||
item.costType === "PACKAGING_MATERIALS",
|
||
)
|
||
.map((item) => {
|
||
// 查找是否在purchaseOrder中已有该item的记录
|
||
const existingItem = value?.find(
|
||
(costItem) => costItem.itemId === item.itemId,
|
||
);
|
||
|
||
return {
|
||
orderCostId: existingItem
|
||
? existingItem.orderCostId
|
||
: generateShortId(),
|
||
itemId: item.itemId,
|
||
name: item.name,
|
||
price: item.price,
|
||
unit: item.unit,
|
||
selected: existingItem ? existingItem.selected : false,
|
||
count: existingItem ? existingItem.count : 1,
|
||
payerType: existingItem ? existingItem.payerType : undefined,
|
||
principal: existingItem ? existingItem.principal : "",
|
||
costType: item.costType,
|
||
requireQuantityAndPrice: existingItem
|
||
? existingItem.requireQuantityAndPrice
|
||
: false,
|
||
};
|
||
}) || [];
|
||
|
||
setCostItemVOList([
|
||
...(initialList || []),
|
||
...(value
|
||
?.filter(
|
||
(item) =>
|
||
item.costType === "WORKER_ADVANCE" ||
|
||
item.costType === "PRODUCTION_ADVANCE",
|
||
)
|
||
.map((item) => {
|
||
return {
|
||
orderCostId: item.orderCostId,
|
||
itemId: item.itemId,
|
||
name: item.name,
|
||
price: item.price,
|
||
unit: item.unit,
|
||
selected: item.selected,
|
||
count: item.count,
|
||
payerType: item.payerType,
|
||
principal: item.principal,
|
||
costType: item.costType,
|
||
requireQuantityAndPrice: item.requireQuantityAndPrice,
|
||
};
|
||
}) || []),
|
||
]);
|
||
|
||
// 初始化总的工头姓名(如果有启用且费用承担方为"我方"的项目)
|
||
const enabledUsItems = initialList.filter(
|
||
(item) =>
|
||
item.selected &&
|
||
item.payerType === "US" &&
|
||
item.costType === "HUMAN_COST",
|
||
);
|
||
if (enabledUsItems.length > 0 && enabledUsItems[0].principal) {
|
||
setPrincipal(enabledUsItems[0].principal || "");
|
||
}
|
||
}
|
||
};
|
||
|
||
// 当传入的value发生变化时,重新初始化列表
|
||
useEffect(() => {
|
||
init().then();
|
||
}, []);
|
||
|
||
// 当内部状态发生变化时,通知父组件更新
|
||
useEffect(() => {
|
||
if (costItemVOList && onChange) {
|
||
// 更新所有启用且费用承担方为"我方"的人工费用项的工头姓名
|
||
const updatedList = costItemVOList.map((item) => {
|
||
if (
|
||
item.costType === "HUMAN_COST" &&
|
||
item.selected &&
|
||
item.payerType === "US"
|
||
) {
|
||
return { ...item, principal };
|
||
}
|
||
return item;
|
||
});
|
||
onChange(updatedList);
|
||
}
|
||
}, [costItemVOList, principal]);
|
||
|
||
// 错误状态
|
||
const [countError, setCountError] = useState<{ [key: string]: boolean }>(
|
||
{},
|
||
);
|
||
const [payerTypeError, setPayerTypeError] = useState<{
|
||
[key: string]: boolean;
|
||
}>({});
|
||
|
||
// 设置人工项目选择状态
|
||
const setArtificialSelect = (id: string, selected: boolean) => {
|
||
if (!costItemVOList) return;
|
||
const newList = costItemVOList.map((item) =>
|
||
item.orderCostId === id ? { ...item, selected } : item,
|
||
);
|
||
setCostItemVOList(newList);
|
||
};
|
||
|
||
// 处理数量变化
|
||
const handleCountChange = (id: string, value: number) => {
|
||
if (!costItemVOList) return;
|
||
const newList = costItemVOList.map((item) =>
|
||
item.orderCostId === id ? { ...item, count: value } : item,
|
||
);
|
||
setCostItemVOList(newList);
|
||
|
||
// 校验数量
|
||
if (costItemVOList.find((item) => item.orderCostId === id)?.selected) {
|
||
validateCount(id, value);
|
||
}
|
||
};
|
||
|
||
// 校验数量
|
||
const validateCount = (id: string, value: number) => {
|
||
const isValid = value > 0;
|
||
setCountError((prev) => ({
|
||
...prev,
|
||
[id]: !isValid,
|
||
}));
|
||
return isValid;
|
||
};
|
||
|
||
// 校验费用承担方
|
||
const validatePayerType = (id: string, value: CostItem["payerType"]) => {
|
||
const isValid = value === "US" || value === "OTHER";
|
||
setPayerTypeError((prev) => ({
|
||
...prev,
|
||
[id]: !isValid,
|
||
}));
|
||
return isValid;
|
||
};
|
||
|
||
// 校验工头姓名
|
||
const validatePrincipal = (value: string) => {
|
||
const isValid = value.trim().length > 0;
|
||
setPrincipalError(!isValid);
|
||
return isValid;
|
||
};
|
||
|
||
// 处理费用承担方变化
|
||
const handlePayerTypeChange = (
|
||
id: string,
|
||
value: CostItem["payerType"],
|
||
) => {
|
||
if (!costItemVOList) return;
|
||
const newList = costItemVOList.map((item) =>
|
||
item.orderCostId === id ? { ...item, payerType: value } : item,
|
||
);
|
||
setCostItemVOList(newList);
|
||
|
||
// 校验费用承担方
|
||
if (costItemVOList.find((item) => item.orderCostId === id)?.selected) {
|
||
validatePayerType(id, value);
|
||
}
|
||
};
|
||
|
||
// 处理工头姓名变化
|
||
const handlePrincipalChange = (value: string) => {
|
||
setPrincipal(value);
|
||
|
||
// 如果有启用且费用承担方为"我方"的项目,则校验工头姓名
|
||
const enabledUsItems = costItemVOList?.filter(
|
||
(item) =>
|
||
item.costType === "HUMAN_COST" &&
|
||
item.selected &&
|
||
item.payerType === "US",
|
||
);
|
||
if (enabledUsItems && enabledUsItems.length > 0) {
|
||
validatePrincipal(value);
|
||
}
|
||
};
|
||
|
||
// 失去焦点时校验数量
|
||
const handleCountBlur = (id: string, value: number) => {
|
||
validateCount(id, value);
|
||
};
|
||
|
||
// 失去焦点时校验工头姓名
|
||
const handlePrincipalBlur = (value: string) => {
|
||
validatePrincipal(value);
|
||
};
|
||
|
||
// 对外暴露的校验方法
|
||
const validate = () => {
|
||
if (!costItemVOList) return true;
|
||
|
||
let isValid = true;
|
||
|
||
costItemVOList.forEach((item) => {
|
||
if (item.selected) {
|
||
// 校验数量
|
||
if (!validateCount(item.orderCostId, item.count)) {
|
||
isValid = false;
|
||
}
|
||
|
||
if (item.costType === "HUMAN_COST") {
|
||
// 校验费用承担方
|
||
if (!validatePayerType(item.orderCostId, item.payerType)) {
|
||
isValid = false;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 校验总的工头姓名(如果有启用且费用承担方为"我方"的项目)
|
||
const enabledUsItems = costItemVOList.filter(
|
||
(item) =>
|
||
item.costType === "HUMAN_COST" &&
|
||
item.selected &&
|
||
item.payerType === "US",
|
||
);
|
||
if (enabledUsItems.length > 0) {
|
||
if (!validatePrincipal(principal)) {
|
||
isValid = false;
|
||
}
|
||
}
|
||
|
||
if (!isValid) {
|
||
Toast.show("toast", {
|
||
icon: "fail",
|
||
title: "提示",
|
||
content: "请完善人工信息后再进行下一步操作",
|
||
});
|
||
}
|
||
|
||
return isValid;
|
||
};
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
validate,
|
||
}));
|
||
|
||
// 获取指定类型的项目列表
|
||
const getItemsByCostType = (costType: string) => {
|
||
return costItemVOList?.filter((item) => item.costType === costType) || [];
|
||
};
|
||
|
||
// 渲染项目列表
|
||
const renderItemList = (items: CostItem[], type: string) => {
|
||
return items.map((item) => (
|
||
<View key={item.orderCostId}>
|
||
<View className={"flex flex-col gap-2.5 rounded-lg bg-white p-2.5"}>
|
||
<View className={"flex flex-row justify-between"}>
|
||
<View className="block text-sm font-normal text-[#000000]">
|
||
{item.name}
|
||
</View>
|
||
<Checkbox
|
||
className={"flex flex-row items-center"}
|
||
checked={item.selected}
|
||
onChange={(checked) => {
|
||
setArtificialSelect(item.orderCostId, checked);
|
||
}}
|
||
>
|
||
<View className={"text-sm font-normal text-[#000000]"}>
|
||
用了
|
||
</View>
|
||
</Checkbox>
|
||
</View>
|
||
|
||
{item.selected && type === "HUMAN_COST" && (
|
||
<View>
|
||
{/* 费用承担方改为按钮形式,参考OrderPackage中的样式 */}
|
||
<View className="flex items-center justify-between">
|
||
<View className="flex-shrink-0 text-sm">谁出钱:</View>
|
||
<View className="flex gap-2">
|
||
<Button
|
||
size="small"
|
||
type={item.payerType === "US" ? "primary" : "default"}
|
||
onClick={() => {
|
||
handlePayerTypeChange(item.orderCostId, "US");
|
||
}}
|
||
>
|
||
我们出
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
type={item.payerType === "OTHER" ? "primary" : "default"}
|
||
onClick={() => {
|
||
handlePayerTypeChange(item.orderCostId, "OTHER");
|
||
}}
|
||
>
|
||
对方出
|
||
</Button>
|
||
</View>
|
||
</View>
|
||
|
||
{payerTypeError[item.orderCostId] && item.selected && (
|
||
<View className="mt-1 text-xs text-red-500">
|
||
请选择谁出这笔钱
|
||
</View>
|
||
)}
|
||
</View>
|
||
)}
|
||
|
||
{item.selected && (
|
||
<View>
|
||
<View className="flex items-center justify-between gap-2">
|
||
<View className="flex-shrink-0 text-sm">用多少:</View>
|
||
<View className="flex items-center">
|
||
<View
|
||
className="rounded-l-button flex !size-12 flex-none items-center justify-center bg-gray-200 text-sm"
|
||
onClick={() => {
|
||
const count = item.count || 1;
|
||
handleCountChange(
|
||
item.orderCostId,
|
||
Math.max(1, count - 1),
|
||
);
|
||
}}
|
||
>
|
||
<Icon name="minus" size={20} />
|
||
</View>
|
||
<View
|
||
className={`flex h-12 w-full shrink items-center border-4 border-gray-200`}
|
||
>
|
||
<Input
|
||
type="number"
|
||
value={item.count.toString()}
|
||
align={"center"}
|
||
placeholder={"用了多少"}
|
||
onChange={(value) => {
|
||
handleCountChange(item.orderCostId, Number(value));
|
||
}}
|
||
formatter={(value) =>
|
||
Math.max(1, Number(value)).toString()
|
||
}
|
||
onBlur={() =>
|
||
handleCountBlur(item.orderCostId, item.count)
|
||
}
|
||
/>
|
||
</View>
|
||
<View
|
||
className="rounded-r-button flex !size-12 flex-none items-center justify-center bg-gray-200 text-sm"
|
||
onClick={() => {
|
||
const count = item.count || 1;
|
||
handleCountChange(item.orderCostId, count + 1);
|
||
}}
|
||
>
|
||
<Icon name="plus" size={20} />
|
||
</View>
|
||
|
||
<View className={"ml-2.5 text-sm text-gray-500"}>
|
||
{item.unit}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{countError[item.orderCostId] && item.selected && (
|
||
<View className="mt-1 text-xs text-red-500">
|
||
请填写正确的数量
|
||
</View>
|
||
)}
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
));
|
||
};
|
||
|
||
// 检查是否有人工费用项被启用且费用承担方为"我方"
|
||
const shouldShowPrincipalInput = () => {
|
||
return (
|
||
costItemVOList?.some(
|
||
(item) =>
|
||
item.costType === "HUMAN_COST" &&
|
||
item.selected &&
|
||
item.payerType === "US",
|
||
) || false
|
||
);
|
||
};
|
||
|
||
return (
|
||
<View className="flex flex-1 flex-col gap-2.5 bg-[#D1D5DB] p-2.5 pt-2.5">
|
||
<View className={"flex flex-1 flex-col gap-2.5"}>
|
||
<View className="text-sm font-bold">人工费用</View>
|
||
<View className="flex items-center rounded-lg border border-blue-200 bg-blue-50 p-2.5">
|
||
<Icon
|
||
className={"mr-1"}
|
||
name="circle-info"
|
||
color={"var(--color-blue-700)"}
|
||
size={18}
|
||
/>
|
||
<View className={"text-sm text-blue-700"}>对方出钱可以不勾选</View>
|
||
</View>
|
||
{renderItemList(getItemsByCostType("HUMAN_COST"), "HUMAN_COST")}
|
||
|
||
{/* 总的工头姓名输入框 */}
|
||
{shouldShowPrincipalInput() && (
|
||
<View>
|
||
<View className="mb-1 text-sm font-medium">工头叫什么名字</View>
|
||
<View
|
||
className={`flex h-12 items-center rounded-md px-3 ${principalError ? "border-2 border-red-500 bg-red-100" : "border-2 border-gray-300 bg-white"}`}
|
||
>
|
||
<Input
|
||
className="text-base"
|
||
type="text"
|
||
placeholder={"工头的名字"}
|
||
value={principal}
|
||
onChange={(value) => {
|
||
handlePrincipalChange(value);
|
||
}}
|
||
onBlur={() => handlePrincipalBlur(principal)}
|
||
/>
|
||
</View>
|
||
{principalError && (
|
||
<View className="mt-1 text-xs text-red-500">
|
||
工头叫啥子填一下
|
||
</View>
|
||
)}
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View className={"flex flex-1 flex-col gap-2.5"}>
|
||
<View className="text-sm font-bold">辅料费用</View>
|
||
<View className="flex items-center rounded-lg border border-blue-200 bg-blue-50 p-2.5">
|
||
<Icon
|
||
className={"mr-1"}
|
||
name="circle-info"
|
||
color={"var(--color-blue-700)"}
|
||
size={18}
|
||
/>
|
||
<View className={"text-sm text-blue-700"}>用了几包就填几包</View>
|
||
</View>
|
||
{renderItemList(
|
||
getItemsByCostType("PACKAGING_MATERIALS"),
|
||
"PACKAGING_MATERIALS",
|
||
)}
|
||
</View>
|
||
{/*<View className={"mb-2.5"}>*/}
|
||
{/* <View className="mb-2.5 text-sm font-bold">空纸箱费用</View>*/}
|
||
{/* {renderItemList(getItemsByCostType("OTHER_COST"), "OTHER_COST")}*/}
|
||
{/*</View>*/}
|
||
{/*<View className={"mb-2.5"}>*/}
|
||
{/* <View className="mb-2.5 text-sm font-bold">空礼盒费用</View>*/}
|
||
{/* {renderItemList(getItemsByCostType("OTHER_COST"), "OTHER_COST")}*/}
|
||
{/*</View>*/}
|
||
|
||
{/* 只有当用户选择"否"时才显示添加按钮 */}
|
||
{!supplierVO.isLast && (
|
||
<Button
|
||
icon={<Icon name={"plus"} size={20} />}
|
||
type={"primary"}
|
||
size={"xlarge"}
|
||
fill={"outline"}
|
||
block
|
||
className="border-primary text-primary flex w-full items-center justify-center !border-4 !bg-white"
|
||
onClick={() => {
|
||
onAdd();
|
||
}}
|
||
>
|
||
<View>添加另一个瓜农</View>
|
||
</Button>
|
||
)}
|
||
</View>
|
||
);
|
||
},
|
||
);
|