ERPTurbo_Client/packages/app-client/src/components/order/module/OrderVehicle.tsx
2025-12-29 23:52:15 +08:00

892 lines
28 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, useImperativeHandle, useState } from "react";
import dayjs from "dayjs";
import Taro from "@tarojs/taro";
import { business } from "@/services";
import { validatePrice as validatePrice1 } from "@/utils";
interface IOrderVehicleProps {
value: BusinessAPI.OrderVO;
onChange: (orderVO: BusinessAPI.OrderVO) => void;
}
export interface OrderVehicleRef {
validate: () => boolean;
}
export default forwardRef<OrderVehicleRef, IOrderVehicleProps>(
function OrderVehicle(props, ref) {
const { value, onChange } = props;
const { orderVehicle, orderDealer } = value;
const [originalData, setOriginalData] = useState<string>();
const [show, setShow] = useState(false);
// 校验状态
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(); // 今天
// 校验手机号码函数
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 || 0);
const isStrawCurtainPriceValid =
!orderVehicle.openStrawCurtain ||
validatePrice(orderVehicle?.strawCurtainPrice || 0);
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 (orderVehicle.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 = (phone: string) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
phone: phone as any,
},
});
// 校验手机号码并更新错误状态
const isValid = validatePhone(phone);
setPhoneError(!isValid && phone !== "");
// 如果手机号有效,清除错误状态
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 = (plate: string) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
plate: plate as any,
},
});
// 校验车牌号并更新错误状态
const isValid = validatePlate(plate);
setPlateError(!isValid && plate !== "");
// 如果车牌号有效,清除错误状态
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 = (driver: string) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
driver: driver as any,
},
});
// 校验司机姓名并更新错误状态
const isValid = validateDriver(driver);
setDriverError(!isValid && driver !== "");
// 如果司机姓名有效,清除错误状态
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 = (origin: string) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
origin: origin as any,
},
});
// 校验出发地并更新错误状态
const isValid = validateOrigin(origin);
setOriginError(!isValid && origin !== "");
// 如果出发地有效,清除错误状态
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 = (destination: string) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
destination: destination as any,
},
});
// 校验目的地并更新错误状态
const isValid = validateDestination(destination);
setDestinationError(!isValid && destination !== "");
// 如果目的地有效,清除错误状态
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 = (price: string) => {
// 只允许数字和小数点,小数点后最多两位
const numValue = validatePrice1(price);
if (numValue !== undefined) {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
price: numValue as any,
},
});
// 校验运费并更新错误状态
const isValid = validatePrice(price);
setPriceError(!isValid && price !== "");
// 如果运费有效,清除错误状态
if (isValid) {
setPriceError(false);
}
}
};
const handlePriceBlur = () => {
if (orderVehicle?.price && !validatePrice(orderVehicle.price)) {
setPriceError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "运费必须大于等于0",
});
} else {
setPriceError(false);
}
};
const handleStrawCurtainPriceChange = (strawCurtainPrice: string) => {
const numValue = validatePrice1(strawCurtainPrice);
if (numValue !== undefined) {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
strawCurtainPrice: numValue as any,
},
});
// 校验草帘费用并更新错误状态
const isValid = validatePrice(strawCurtainPrice);
setStrawCurtainPriceError(!isValid && strawCurtainPrice !== "");
// 如果草帘费用有效,清除错误状态
if (isValid) {
setStrawCurtainPriceError(false);
}
}
};
const handleStrawCurtainPriceBlur = () => {
if (
orderVehicle?.strawCurtainPrice &&
!validatePrice(orderVehicle.strawCurtainPrice)
) {
setStrawCurtainPriceError(true);
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "草帘费用必须大于等于0",
});
} else {
setStrawCurtainPriceError(false);
}
};
const confirm = (_values: PickerValue[]) => {
const selectedDate = dayjs(_values.join("-")).format("YYYY-MM-DD");
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
deliveryTime: selectedDate as any,
},
});
// 校验发货时间
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 (originalData: string) => {
// 简单校验message避免无效的数据抽取
if (!originalData || originalData.trim() === "") {
Toast.show("toast", {
title: "请输入有效内容",
icon: "none",
});
return;
}
Taro.showLoading({
title: "识别中,请稍后",
});
const {
data: { data: newVehicle },
} = await business.extraction.vehicleExtraction({
message: originalData,
dealerNames: "",
});
if (newVehicle) {
const dealerVO = newVehicle.dealerVO;
onChange?.({
...value,
orderVehicle: {
...newVehicle,
// 通过 - 分割
plate: newVehicle.plate?.split("-")[0]!,
dealerId: newVehicle.dealerId || orderVehicle?.dealerId,
dealerName: newVehicle.dealerName || orderVehicle?.dealerName,
openStrawCurtain: false,
//@ts-ignore
strawCurtainPrice: "",
deliveryTime: dayjs().format("YYYY-MM-DD"),
originalData: originalData || "",
},
orderDealer: {
...orderDealer,
...dealerVO,
},
});
// 清除所有错误状态
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="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className={"flex flex-1 flex-col gap-2.5"}>
<View className="text-primary text-base font-bold"></View>
<View
className={
"flex w-full items-center rounded-md border-4 border-gray-300"
}
>
<TextArea
className={"flex-1"}
value={originalData}
onChange={(value) => {
setOriginalData(value);
}}
rows={8}
maxLength={500}
showCount
placeholder="「粘贴识别」获输入文本,智能拆分车牌号、司机姓名、联系电话、出发地、目的地和运费"
/>
</View>
</View>
<View className={"flex flex-row gap-2.5"}>
{originalData ? (
<>
<View className={"flex-1"}>
<Button
size={"large"}
type={"default"}
block
className="bg-primary flex h-10 items-center justify-center px-4 text-white"
onClick={async () => {
setOriginalData("");
}}
>
</Button>
</View>
<View className={"flex-1"}>
<Button
id={"target1"}
size={"large"}
type={"primary"}
block
className="bg-primary flex h-10 items-center justify-center px-4 text-white"
onClick={async () => {
await vehicleExtraction(originalData);
}}
>
</Button>
</View>
</>
) : (
<View className={"flex-1"}>
<Button
id={"target1"}
size={"large"}
type={"primary"}
block
className="bg-primary flex h-10 items-center justify-center px-4 text-white"
onClick={() => {
Taro.getClipboardData({
success: async (res) => {
let originalData = res.data;
if (originalData == "") {
return;
}
await vehicleExtraction(originalData);
setOriginalData(originalData);
},
});
}}
>
</Button>
</View>
)}
</View>
</View>
</View>
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className="text-primary text-base font-bold"></View>
<View className={"flex flex-col gap-2.5"}>
<View className="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
clearable
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={"flex flex-col gap-2.5"}>
<View className="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
clearable
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={"flex flex-col gap-2.5"}>
<View className="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
clearable
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={"flex flex-col gap-2.5"}>
<View className="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
clearable
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={"flex flex-col gap-2.5"}>
<View className="block text-sm font-normal text-[#000000]">
</View>
<View className={"flex flex-row gap-2.5"}>
<View className={"flex-1"}>
<View
id={"target2"}
className={`flex h-10 w-full flex-1 items-center rounded-md ${dealerNameError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<DealerPicker
onFinish={(dealerVO) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
dealerId: dealerVO.dealerId,
dealerName: dealerVO.shortName!,
},
orderDealer: {
...orderDealer,
...dealerVO,
},
});
// 校验经销商信息
const isValid = validateDealerName(dealerVO.shortName!);
setDealerNameError(!isValid);
}}
enableManualInput
trigger={
<View
className={
"flex flex-1 flex-row items-center justify-between px-5"
}
>
<View className={"text-sm"}>
{orderVehicle?.dealerName || "选择经销商"}
</View>
<Icon name={"chevron-down"} />
</View>
}
/>
</View>
{dealerNameError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
<View className={"flex-1"}>
<View
className={`flex h-10 w-full items-center rounded-md ${destinationError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
clearable
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={"flex flex-col gap-2.5"}>
<View className="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
clearable
type="digit"
placeholder="请输入运费可填0"
value={orderVehicle?.price?.toString()}
onChange={handlePriceChange}
onBlur={handlePriceBlur}
/>
<View
className="text-gray-500"
style={{
padding: `var(--nutui-input-padding)`,
}}
>
</View>
</View>
{priceError && (
<View className="mt-1 text-xs text-red-500">
0
</View>
)}
</View>
<View className={"flex flex-col gap-2.5"}>
<View className={"flex flex-row justify-between"}>
<View className="block text-sm font-normal text-[#000000]">
</View>
<Checkbox
className={"flex flex-row items-center"}
checked={orderVehicle?.openStrawCurtain}
onChange={(checked) => {
onChange?.({
...value,
orderVehicle: {
...orderVehicle,
openStrawCurtain: checked,
},
});
}}
>
<View className={"text-sm font-normal text-[#000000]"}>
</View>
</Checkbox>
</View>
{orderVehicle?.openStrawCurtain && (
<View
className={`flex h-10 w-full items-center rounded-md ${strawCurtainPriceError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
clearable
type="digit"
placeholder="请输入草帘费用"
value={orderVehicle?.strawCurtainPrice?.toString()}
onChange={handleStrawCurtainPriceChange}
onBlur={handleStrawCurtainPriceBlur}
/>
<View
className="text-gray-500"
style={{
padding: `var(--nutui-input-padding)`,
}}
>
</View>
</View>
)}
{strawCurtainPriceError && orderVehicle?.openStrawCurtain && (
<View className="mt-1 text-xs text-red-500">
0
</View>
)}
</View>
<View className={"flex flex-col gap-2.5"}>
<View className="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"}`}
>
<View
className={
"flex flex-1 flex-row items-center justify-between px-5"
}
style={{
color: "var(--nutui-color-title, #1a1a1a)",
}}
onClick={() => setShow(true)}
>
<View className={"text-sm"}>
{orderVehicle?.deliveryTime
? dayjs(orderVehicle?.deliveryTime).format(
"YYYY年MM月DD日",
)
: "请选择发货时间"}
</View>
<Icon name={"chevron-down"} />
</View>
</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>
</View>
);
},
);