ERPTurbo_Client/packages/app-client/src/components/purchase/module/Weigh.tsx
shenyifei 5a814cb358 feat(purchase): 重构采购模块组件结构与交互逻辑
- 将采购相关组件移至 module 目录统一管理
- 优化 OrderCost 组件的人工费用处理逻辑,改为统一管理工头姓名
- 改进 OrderPackage 组件的纸箱类型渲染逻辑,根据供应商属性动态显示
- 更新 Weigh 组件,支持多供应商场景下的纸箱选择展示
- 调整采购模块导出路径,适配新的目录结构
- 优化表单交互文案,提升用户体验一致性
2025-11-05 10:21:11 +08:00

510 lines
16 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 { View } from "@tarojs/components";
import {
Input,
Radio,
Toast,
Uploader,
UploaderFileItem,
} from "@nutui/nutui-react-taro";
import { Icon } from "@/components";
import { SupplierVO } from "@/types/typings";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { uploadFile } from "@/utils/uploader";
import { validatePrice } from "@/utils/format";
// 定义ref暴露的方法接口
export interface WeighRef {
validate: () => boolean;
}
interface IWeightProps {
value: SupplierVO;
onChange: (supplierVO: SupplierVO) => void;
supplierCount: number;
}
export default forwardRef<WeighRef, IWeightProps>(function Weigh(props, ref) {
const { value, onChange, supplierCount } = props;
const [supplierVO, setSupplierVO] = useState<SupplierVO>();
// 初始化数据
useEffect(() => {
setSupplierVO(value);
// 初始化空磅照片和总磅照片
if (value.emptyWeightImg) {
setEmptyWeightImgList([
{
url: value.emptyWeightImg,
name: "empty-weight-image",
status: "success",
},
]);
}
if (value.totalWeightImg) {
setTotalWeightImgList([
{
url: value.totalWeightImg,
name: "total-weight-image",
status: "success",
},
]);
}
}, []);
// 监听供应商信息变化
useEffect(() => {
if (supplierVO) {
onChange(supplierVO);
}
}, [supplierVO]);
const [emptyWeightImgList, setEmptyWeightImgList] = useState<
UploaderFileItem[]
>([]);
const [totalWeightImgList, setTotalWeightImgList] = useState<
UploaderFileItem[]
>([]);
const [isPaperError, setIsPaperError] = useState<{
[key: string]: boolean;
}>({}); // 添加是否为最后一个瓜农的错误状态
// 添加三个字段的错误状态
const [emptyWeightError, setEmptyWeightError] = useState<{
[key: string]: boolean;
}>({});
const [totalWeightError, setTotalWeightError] = useState<{
[key: string]: boolean;
}>({});
const [priceError, setPriceError] = useState<{
[key: string]: boolean;
}>({});
// 空磅照片变更处理函数
const handleEmptyWeightImgChange = (files: UploaderFileItem[]) => {
setEmptyWeightImgList(files);
// 如果有文件且上传成功保存URL到supplierVO
if (files.length > 0 && files[0].url) {
setSupplierVO((prev) => ({
...prev!,
emptyWeightImg: files[0].url,
}));
} else {
// 如果没有文件清空URL
setSupplierVO((prev) => ({
...prev!,
emptyWeightImg: undefined,
}));
}
};
// 总磅照片变更处理函数
const handleTotalWeightImgChange = (files: UploaderFileItem[]) => {
setTotalWeightImgList(files);
// 如果有文件且上传成功保存URL到supplierVO
if (files.length > 0 && files[0].url) {
setSupplierVO((prev) => ({
...prev!,
totalWeightImg: files[0].url,
}));
} else {
// 如果没有文件清空URL
setSupplierVO((prev) => ({
...prev!,
totalWeightImg: undefined,
}));
}
};
// 将校验方法暴露给父组件
useImperativeHandle(ref, () => ({
validate,
}));
if (!supplierVO) {
return;
}
// 校验是否为最后一个瓜农函数
const validateIsPaper = (isPaper: any) => {
// 必须选择是或否
return isPaper !== undefined;
};
// 校验空磅重量
const validateEmptyWeight = (value: any) => {
if (value === "" || value === undefined || value === null) {
setEmptyWeightError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: true,
}));
return false;
}
const numValue = Number(value);
if (Number.isNaN(numValue) || numValue < 0) {
setEmptyWeightError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: true,
}));
return false;
}
setEmptyWeightError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: false,
}));
return true;
};
// 校验总磅重量
const validateTotalWeight = (value: any) => {
if (value === "" || value === undefined || value === null) {
setTotalWeightError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: true,
}));
return false;
}
const numValue = Number(value);
if (Number.isNaN(numValue) || numValue < 0) {
setTotalWeightError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: true,
}));
return false;
}
setTotalWeightError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: false,
}));
return true;
};
// 校验采购单价
const validatePurchasePrice = (value: any) => {
if (value === "" || value === undefined || value === null) {
setPriceError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: true,
}));
return false;
}
const numValue = Number(value);
if (Number.isNaN(numValue) || numValue < 0) {
setPriceError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: true,
}));
return false;
}
setPriceError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: false,
}));
return true;
};
// 对外暴露的校验方法
const validate = () => {
const id = supplierVO?.orderSupplierId;
if (!id) return false;
const isEmptyWeightValid = validateEmptyWeight(supplierVO.emptyWeight);
const isTotalWeightValid = validateTotalWeight(supplierVO.totalWeight);
const isPurchasePriceValid = validatePurchasePrice(
supplierVO.purchasePrice,
);
const isPaperValid = validateIsPaper(supplierVO.isPaper);
// 更新错误状态
setIsPaperError((prev) => ({
...prev,
[id]: !isPaperValid,
}));
setPriceError((prev) => ({
...prev,
[id]: !isPurchasePriceValid,
}));
setTotalWeightError((prev) => ({
...prev,
[id]: !isTotalWeightValid,
}));
setEmptyWeightError((prev) => ({
...prev,
[id]: !isEmptyWeightValid,
}));
const isValid =
isEmptyWeightValid &&
isTotalWeightValid &&
isPurchasePriceValid &&
isPaperValid;
if (!isValid) {
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请完善称重信息后再进行下一步操作",
});
}
return isValid;
};
return (
<View className="flex flex-1 flex-col gap-2.5 p-2.5">
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className={"flex items-center justify-between gap-2.5"}>
<View className="text-sm"></View>
<View className="text-neutral-darkest text-sm font-medium">
{supplierCount == 1 ? (
<Radio.Group
direction="horizontal"
value={
supplierVO.isPaper === true
? "true"
: supplierVO.isPaper === false
? "false"
: undefined
}
onChange={(value) => {
// 清除错误状态
setIsPaperError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: false,
}));
// 根据用户选择设置是否包含空纸箱
const isPaperValue =
value === "true"
? true
: value === "false"
? false
: undefined;
setSupplierVO({
...supplierVO,
isPaper: isPaperValue,
});
}}
>
<Radio value="true"></Radio>
<Radio value="false"></Radio>
</Radio.Group>
) : supplierVO.isPaper === true ? (
"带了"
) : (
"没带"
)}
</View>
</View>
{isPaperError[supplierVO.orderSupplierId] && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="text-primary mb-2.5 text-base font-bold">
{supplierVO.name}
</View>
<View className="mb-2.5">
<View className="mb-2.5 flex gap-2.5">
<View className="flex-1 rounded-lg bg-blue-50 p-2.5">
<View className="mb-2.5 flex items-center">
<Icon
name={"truck"}
className="mr-2"
size={20}
color={"var(--color-blue-500)"}
/>
<View className="text-sm text-gray-700"> (kg)</View>
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${emptyWeightError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-blue-300"}`}
>
<Input
placeholder="0"
type="digit"
value={supplierVO.emptyWeight?.toString()}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setSupplierVO({
...supplierVO,
emptyWeight: numValue as any,
});
// 校验输入值
validateEmptyWeight(numValue);
}
}}
onBlur={() => {
// 失去焦点时进行校验
validateEmptyWeight(supplierVO.emptyWeight);
}}
/>
</View>
{emptyWeightError[supplierVO.orderSupplierId] && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
<View className="mt-2.5 flex flex-row text-xs text-blue-600">
<Icon
name={"circle-info"}
className="mr-1"
color={"rgb(--color-blue-600)"}
size={12}
/>
<View></View>
</View>
</View>
<View className="flex-1 rounded-lg bg-green-50 p-2.5">
<View className="mb-2.5 flex items-center">
<Icon
name={"weight-scale"}
className="mr-2"
size={20}
color={"var(--color-green-300)"}
/>
<View className="text-sm text-gray-700"> (kg)</View>
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${totalWeightError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-green-300"}`}
>
<Input
placeholder="0"
type="digit"
value={supplierVO.totalWeight?.toString()}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setSupplierVO({
...supplierVO,
totalWeight: numValue as any,
});
// 校验输入值
validateTotalWeight(numValue);
}
}}
onBlur={() => {
// 失去焦点时进行校验
validateTotalWeight(supplierVO.totalWeight);
}}
/>
</View>
{totalWeightError[supplierVO.orderSupplierId] && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
</View>
<View className="mb-2.5 space-y-2.5">
<View className="rounded-lg bg-yellow-50 p-2.5">
<View className="mb-2.5 flex items-center">
<Icon
name={"money-bill"}
className="mr-2"
size={20}
color={"var(--color-yellow-500)"}
/>
<View className="text-sm text-gray-700">
(/)
</View>
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${priceError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-yellow-300"}`}
>
<Input
placeholder="0.00"
type="digit"
value={supplierVO.purchasePrice?.toString()}
onChange={(value) => {
const numValue = validatePrice(value);
if (numValue !== undefined) {
setSupplierVO({
...supplierVO,
purchasePrice: numValue as any,
});
// 校验输入值
validatePurchasePrice(numValue);
}
}}
onBlur={() => {
// 失去焦点时进行校验
validatePurchasePrice(supplierVO.purchasePrice);
}}
/>
</View>
{priceError[supplierVO.orderSupplierId] && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
</View>
</View>
</View>
<View className={"flex flex-row gap-2.5"}>
<View className="border-primary flex-1 rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className={`flex w-full border-gray-300`}>
<Uploader
className={"w-full"}
value={emptyWeightImgList}
onChange={handleEmptyWeightImgChange}
sourceType={["album", "camera"]}
uploadIcon={<Icon name={"camera"} size={36} />}
uploadLabel={
<View className={"flex flex-col items-center"}>
<View className="text-sm"></View>
</View>
}
maxCount={1}
//@ts-ignore
upload={uploadFile}
multiple
/>
</View>
</View>
<View className="border-primary flex-1 rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className={`flex w-full border-gray-300`}>
<Uploader
className={"w-full"}
value={totalWeightImgList}
onChange={handleTotalWeightImgChange}
sourceType={["album", "camera"]}
uploadIcon={<Icon name={"camera"} size={36} />}
uploadLabel={
<View className={"flex flex-col items-center"}>
<View className="text-sm"></View>
</View>
}
maxCount={1}
//@ts-ignore
upload={uploadFile}
multiple
/>
</View>
</View>
</View>
</View>
);
});