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

817 lines
26 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 {
Button,
Checkbox,
DatePicker,
Input,
PickerOption,
PickerValue,
TextArea,
Toast,
} from "@nutui/nutui-react-taro";
import { DealerPicker, Icon } from "@/components";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import dayjs from "dayjs";
import Taro from "@tarojs/taro";
import { business } from "@/services";
import { validatePrice as validatePrice1 } from "@/utils/format";
interface IOrderVehicleProps {
orderVehicle: BusinessAPI.OrderVehicle;
setOrderVehicle: (orderVehicle: BusinessAPI.OrderVehicle) => void;
orderDealer: BusinessAPI.OrderDealer;
setOrderDealer: (orderDealer: BusinessAPI.OrderDealer) => void;
}
export interface OrderVehicleRef {
validate: () => boolean;
}
export default forwardRef<OrderVehicleRef, IOrderVehicleProps>(
function OrderVehicle(props, ref) {
const { orderVehicle, setOrderVehicle, setOrderDealer, orderDealer } =
props;
const [dealerVO, setDealerVO] = useState<BusinessAPI.DealerVO>();
const [openStrawCurtain, setOpenStrawCurtain] = useState<boolean>(false);
const [text, setText] = useState<string>();
const [show, setShow] = useState(false);
const [desc, setDesc] = useState<string>("");
// 校验状态
const [phoneError, setPhoneError] = useState(false);
const [plateError, setPlateError] = useState(false);
const [driverError, setDriverError] = useState(false);
const [originError, setOriginError] = useState(false);
const [destinationError, setDestinationError] = useState(false);
const [priceError, setPriceError] = useState(false);
const [strawCurtainPriceError, setStrawCurtainPriceError] = useState(false);
const [deliveryTimeError, setDeliveryTimeError] = useState(false);
const [dealerNameError, setDealerNameError] = useState(false);
// 获取当年的第一天和今天,用于限制日期选择范围
const currentYear = new Date().getFullYear();
const startDate = new Date(currentYear, 0, 1); // 当年第一天
const endDate = new Date(); // 今天
// 当desc改变时更新车辆信息中的时间
useEffect(() => {
if (desc) {
setOrderVehicle({
...orderVehicle,
deliveryTime: desc,
});
}
}, [desc]);
// 当dealerVO改变时更新车辆信息中的经销商信息
useEffect(() => {
if (dealerVO) {
setOrderVehicle({
...orderVehicle,
dealerId: dealerVO.dealerId,
dealerName: dealerVO.shortName!,
});
setOrderDealer({
...orderDealer,
dealerId: dealerVO.dealerId,
shortName: dealerVO?.shortName!,
dealerType: dealerVO.dealerType!,
enableShare: dealerVO.enableShare,
shareRatio: dealerVO.shareRatio,
freightCostFlag: dealerVO.freightCostFlag,
strawMatCostFlag: dealerVO.strawMatCostFlag,
includePackingFlag: dealerVO.includePackingFlag,
documentTypes: dealerVO.documentTypes,
// 清空账户和仓库信息,因为经销商已更改
accountId: undefined,
companyName: undefined,
taxNumber: undefined,
bankAccount: undefined,
companyAddress: undefined,
phone: undefined,
openingBank: undefined,
warehouseId: undefined,
warehouseName: undefined,
warehouseAddress: undefined,
contactPerson: undefined,
contactPhone: undefined,
receiverName: undefined,
receiverPhone: undefined,
});
// 校验经销商信息
const isValid = validateDealerName(dealerVO.shortName!);
setDealerNameError(!isValid);
}
}, [dealerVO]);
// 校验手机号码函数
const validatePhone = (phone: string) => {
if (!phone) {
return false;
}
const phoneRegex = /^1[3456789]\d{9}$/;
return phoneRegex.test(phone);
};
// 校验车牌号函数
const validatePlate = (plate: string) => {
if (!plate) {
return false;
}
// 车牌号正则表达式(包含普通蓝牌、黄牌、绿牌、黑牌、白牌等)
const plateRegex =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]$/;
return plateRegex.test(plate);
};
// 校验司机姓名函数
const validateDriver = (driver: string) => {
if (!driver) {
return false;
}
// 司机姓名至少2个字符
return driver.length >= 2;
};
// 校验出发地函数
const validateOrigin = (origin: string) => {
if (!origin) {
return false;
}
// 出发地至少2个字符
return origin.length >= 2;
};
// 校验目的地函数
const validateDestination = (destination: string) => {
if (!destination) {
return false;
}
// 目的地至少2个字符
return destination.length >= 2;
};
// 校验运费函数
const validatePrice = (price: string | number) => {
if (price === "" || price === undefined) {
return false;
}
const priceNum = typeof price === "string" ? parseFloat(price) : price;
// 运费必须大于0
return !Number.isNaN(priceNum) && priceNum > 0;
};
// 校验发货时间函数
const validateDeliveryTime = (deliveryTime: string) => {
return !!deliveryTime; // 发货时间必须存在
};
// 校验经销商函数
const validateDealerName = (dealerName: string) => {
return !!dealerName; // 经销商必须存在
};
// 对外暴露的校验方法
const validate = () => {
console.log("vehicle", orderVehicle);
const isPhoneValid = validatePhone(orderVehicle?.phone || "");
const isPlateValid = validatePlate(orderVehicle?.plate || "");
const isDriverValid = validateDriver(orderVehicle?.driver || "");
const isOriginValid = validateOrigin(orderVehicle?.origin || "");
const isDestinationValid = validateDestination(
orderVehicle?.destination || "",
);
const isPriceValid = validatePrice(orderVehicle?.price || "");
const isStrawCurtainPriceValid =
!openStrawCurtain ||
validatePrice(orderVehicle?.strawCurtainPrice || "");
const isDeliveryTimeValid = validateDeliveryTime(
orderVehicle?.deliveryTime || "",
);
const isDealerValid = validateDealerName(orderVehicle?.dealerName);
setPhoneError(!isPhoneValid);
setPlateError(!isPlateValid);
setDriverError(!isDriverValid);
setOriginError(!isOriginValid);
setDestinationError(!isDestinationValid);
setPriceError(!isPriceValid);
setDeliveryTimeError(!isDeliveryTimeValid);
setDealerNameError(!isDealerValid);
if (openStrawCurtain) {
setStrawCurtainPriceError(!isStrawCurtainPriceValid);
}
const isValid =
isPhoneValid &&
isPlateValid &&
isDriverValid &&
isOriginValid &&
isDestinationValid &&
isPriceValid &&
isStrawCurtainPriceValid &&
isDeliveryTimeValid &&
isDealerValid;
if (!isValid) {
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请完善车辆信息后再进行下一步操作",
});
}
return isValid;
};
useImperativeHandle(ref, () => ({
validate,
}));
const handlePhoneChange = (value: string) => {
// 更新手机号码
setOrderVehicle({
...orderVehicle,
phone: value,
});
// 校验手机号码并更新错误状态
const isValid = validatePhone(value);
setPhoneError(!isValid && value !== "");
// 如果手机号有效,清除错误状态
if (isValid) {
setPhoneError(false);
}
};
const handlePhoneBlur = () => {
// 失去焦点时校验手机号码
if (orderVehicle?.phone && !validatePhone(orderVehicle.phone)) {
setPhoneError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请输入正确的手机号码",
});
} else {
setPhoneError(false);
}
};
const handlePlateChange = (value: string) => {
setOrderVehicle({
...orderVehicle,
plate: value.toUpperCase(), // 车牌号转为大写
});
// 校验车牌号并更新错误状态
const isValid = validatePlate(value);
setPlateError(!isValid && value !== "");
// 如果车牌号有效,清除错误状态
if (isValid) {
setPlateError(false);
}
};
const handlePlateBlur = () => {
if (orderVehicle?.plate && !validatePlate(orderVehicle.plate)) {
setPlateError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请输入正确的车牌号",
});
} else {
setPlateError(false);
}
};
const handleDriverChange = (value: string) => {
setOrderVehicle({
...orderVehicle,
driver: value,
});
// 校验司机姓名并更新错误状态
const isValid = validateDriver(value);
setDriverError(!isValid && value !== "");
// 如果司机姓名有效,清除错误状态
if (isValid) {
setDriverError(false);
}
};
const handleDriverBlur = () => {
if (orderVehicle?.driver && !validateDriver(orderVehicle.driver)) {
setDriverError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "司机姓名至少2个字符",
});
} else {
setDriverError(false);
}
};
const handleOriginChange = (value: string) => {
setOrderVehicle({
...orderVehicle,
origin: value,
});
// 校验出发地并更新错误状态
const isValid = validateOrigin(value);
setOriginError(!isValid && value !== "");
// 如果出发地有效,清除错误状态
if (isValid) {
setOriginError(false);
}
};
const handleOriginBlur = () => {
if (orderVehicle?.origin && !validateOrigin(orderVehicle.origin)) {
setOriginError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "出发地至少2个字符",
});
} else {
setOriginError(false);
}
};
const handleDestinationChange = (value: string) => {
setOrderVehicle({
...orderVehicle,
destination: value,
});
// 校验目的地并更新错误状态
const isValid = validateDestination(value);
setDestinationError(!isValid && value !== "");
// 如果目的地有效,清除错误状态
if (isValid) {
setDestinationError(false);
}
};
const handleDestinationBlur = () => {
if (
orderVehicle?.destination &&
!validateDestination(orderVehicle.destination)
) {
setDestinationError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "目的地至少2个字符",
});
} else {
setDestinationError(false);
}
};
const handlePriceChange = (value: string) => {
// 只允许数字和小数点,小数点后最多两位
const numValue = validatePrice1(value);
if (numValue !== undefined) {
setOrderVehicle({
...orderVehicle,
price: numValue as any,
});
// 校验运费并更新错误状态
const isValid = validatePrice(value);
setPriceError(!isValid && value !== "");
// 如果运费有效,清除错误状态
if (isValid) {
setPriceError(false);
}
}
};
const handlePriceBlur = () => {
if (orderVehicle?.price && !validatePrice(orderVehicle.price)) {
setPriceError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请输入正确的运费金额",
});
} else {
setPriceError(false);
}
};
const handleStrawCurtainPriceChange = (value: string) => {
const numValue = validatePrice1(value);
if (numValue !== undefined) {
setOrderVehicle({
...orderVehicle,
strawCurtainPrice: numValue as any,
});
// 校验草帘费用并更新错误状态
const isValid = validatePrice(value);
setStrawCurtainPriceError(!isValid && value !== "");
// 如果草帘费用有效,清除错误状态
if (isValid) {
setStrawCurtainPriceError(false);
}
}
};
const handleStrawCurtainPriceBlur = () => {
if (
orderVehicle?.strawCurtainPrice &&
!validatePrice(orderVehicle.strawCurtainPrice)
) {
setStrawCurtainPriceError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请输入正确的草帘费用",
});
} else {
setStrawCurtainPriceError(false);
}
};
const confirm = (_values: PickerValue[]) => {
const selectedDate = dayjs(_values.join("-")).format("YYYY-MM-DD");
setDesc(selectedDate);
// 校验发货时间
const isValid = validateDeliveryTime(selectedDate);
setDeliveryTimeError(!isValid);
};
const formatter = (type: string, option: PickerOption) => {
switch (type) {
case "year":
option.label += "年";
break;
case "month":
option.label += "月";
break;
case "day":
option.label += "日";
break;
case "hour":
option.label += "时";
break;
case "minute":
option.label += "分";
break;
default:
break;
}
return option;
};
const vehicleExtraction = async (message: string) => {
// 简单校验message避免无效的数据抽取
if (!message || message.trim() === "") {
Taro.showToast({
title: "请输入有效内容",
icon: "none",
});
return;
}
Taro.showLoading({
title: "识别中,请稍后",
});
const { data } = await business.extraction.vehicleExtraction({
message: message,
});
const newVehicle = data.data as BusinessAPI.OrderVehicle;
setOrderVehicle({
...newVehicle,
// 通过 - 分割
plate: newVehicle.plate?.split("-")[0],
dealerId: orderVehicle?.dealerId,
dealerName: orderVehicle?.dealerName,
openStrawCurtain: false,
//@ts-ignore
strawCurtainPrice: "",
deliveryTime: dayjs().format("YYYY-MM-DD"),
});
// 清除所有错误状态
setPlateError(false);
setDriverError(false);
setPhoneError(false);
setOriginError(false);
setDestinationError(false);
setPriceError(false);
setStrawCurtainPriceError(false);
setDeliveryTimeError(false);
setDealerNameError(false);
Taro.hideLoading();
};
return (
<View className="flex flex-1 flex-col gap-2.5 p-2.5">
<View className="rounded-lg bg-white p-2.5 shadow-sm">
<View className="relative">
<TextArea
value={text}
onChange={(value) => {
setText(value);
}}
autoSize
className="min-h-20 w-full !border !border-none p-2.5 !text-lg"
placeholder="「粘贴识别」获输入文本,智能拆分车牌号、司机姓名、联系电话、出发地、目的地和运费"
/>
<View className="absolute right-0 bottom-0 z-9 flex gap-2">
{text ? (
<Button
size={"large"}
type={"primary"}
className="bg-primary flex h-10 items-center justify-center px-4 text-white"
onClick={async () => {
await vehicleExtraction(text);
}}
>
<View></View>
</Button>
) : (
<Button
size={"large"}
type={"primary"}
className="bg-primary flex h-10 items-center justify-center px-4 text-white"
onClick={() => {
Taro.getClipboardData({
success: async (res) => {
let message = res.data;
if (message == "") {
return;
}
await vehicleExtraction(message);
setText(message);
},
});
}}
>
<View></View>
</Button>
)}
</View>
</View>
</View>
<View className="rounded-lg bg-white p-2.5 shadow-sm">
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${plateError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="text"
placeholder="请输入车牌号"
value={orderVehicle?.plate}
onChange={handlePlateChange}
onBlur={handlePlateBlur}
/>
</View>
{plateError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${driverError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="text"
placeholder="请输入司机姓名"
value={orderVehicle?.driver}
onChange={handleDriverChange}
onBlur={handleDriverBlur}
/>
</View>
{driverError && (
<View className="mt-1 text-xs text-red-500">
2
</View>
)}
</View>
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${phoneError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="tel"
placeholder="请输入联系电话"
value={orderVehicle?.phone}
onChange={handlePhoneChange}
onBlur={handlePhoneBlur}
/>
</View>
{phoneError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${originError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="text"
placeholder="请输入出发地"
value={orderVehicle?.origin}
onChange={handleOriginChange}
onBlur={handleOriginBlur}
/>
</View>
{originError && (
<View className="mt-1 text-xs text-red-500">
2
</View>
)}
</View>
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View className={"flex flex-row gap-2.5"}>
<View>
<View
className={`relative flex h-10 w-full items-center rounded-md ${dealerNameError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<DealerPicker
onFinish={setDealerVO}
enableManualInput
trigger={
<Input
type="text"
placeholder="选择经销商"
value={orderVehicle?.dealerName}
disabled
/>
}
/>
<Icon
name={"chevron-down"}
className={"absolute -right-1 mr-4"}
/>
</View>
{dealerNameError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View>
<View
className={`flex h-10 w-full items-center rounded-md ${destinationError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="text"
placeholder="请输入目的地"
value={orderVehicle?.destination}
onChange={(value) => handleDestinationChange(value)}
onBlur={handleDestinationBlur}
/>
</View>
{destinationError && (
<View className="mt-1 text-xs text-red-500">
2
</View>
)}
</View>
</View>
</View>
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${priceError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="digit"
placeholder="请输入运费"
value={orderVehicle?.price?.toString()}
onChange={handlePriceChange}
onBlur={handlePriceBlur}
/>
</View>
{priceError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View className="mb-2.5">
<View className={"flex flex-row justify-between"}>
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<Checkbox
className={"flex flex-row items-center"}
onChange={(checked) => {
setOpenStrawCurtain(checked);
}}
>
<View className={"text-sm font-normal text-[#000000]"}>
</View>
</Checkbox>
</View>
{openStrawCurtain && (
<View
className={`flex h-10 w-full items-center rounded-md ${strawCurtainPriceError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="digit"
placeholder="请输入草帘费用"
value={orderVehicle?.strawCurtainPrice?.toString()}
onChange={handleStrawCurtainPriceChange}
onBlur={handleStrawCurtainPriceBlur}
/>
</View>
)}
{strawCurtainPriceError && openStrawCurtain && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View className="mb-2.5">
<View className="mb-2 block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${deliveryTimeError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
type="text"
placeholder="请选择发货时间"
value={
orderVehicle?.deliveryTime
? dayjs(orderVehicle?.deliveryTime).format("YYYY年MM月DD日")
: "请选择发货时间"
}
disabled
onClick={() => setShow(true)}
/>
</View>
{deliveryTimeError && (
<View className="mt-1 text-xs text-red-500"></View>
)}
<DatePicker
title="发货时间选择"
type="date"
startDate={startDate}
endDate={endDate}
visible={show}
defaultValue={new Date()}
formatter={formatter}
onClose={() => setShow(false)}
onConfirm={(_, values) => confirm(values)}
/>
</View>
</View>
</View>
);
},
);