refactor(order): 为订单表单组件添加表单验证功能

- 为 BasicInfoSection 组件添加 validateForm 方法,校验本车次号和运费类型
- 为 CompanyInfoSection 组件添加 validateForm 方法,校验销售方选择
- 为 DealerInfoSection 组件添加 validateForm 方法,校验经销商选择
- 为 DeliveryFormSection 组件添加 validateForm 方法,调用内部表单验证
- 为 MarketPriceSection 组件添加 validateForm 方法,校验报价方式和销售单价
- 在审核页面中使用各组件的验证方法替代原有验证逻辑
- 为各组件导出对应的 Ref 类型定义
- 更新应用版本号从 v0.0.64 到 v0.0.65
This commit is contained in:
shenyifei 2026-01-04 13:54:55 +08:00
parent 5060811144
commit 2aa7df132a
9 changed files with 228 additions and 103 deletions

View File

@ -34,8 +34,11 @@ export { default as OrderWithdrawReview } from "./button/OrderWithdrawReview";
export { default as OrderFinalApprove } from "./button/OrderFinalApprove"; export { default as OrderFinalApprove } from "./button/OrderFinalApprove";
export { default as CompanyInfoSection } from "./section/CompanyInfoSection"; export { default as CompanyInfoSection } from "./section/CompanyInfoSection";
export type { CompanyInfoSectionRef } from "./section/CompanyInfoSection";
export { default as DealerInfoSection } from "./section/DealerInfoSection"; export { default as DealerInfoSection } from "./section/DealerInfoSection";
export type { DealerInfoSectionRef } from "./section/DealerInfoSection";
export { default as BasicInfoSection } from "./section/BasicInfoSection"; export { default as BasicInfoSection } from "./section/BasicInfoSection";
export type { BasicInfoSectionRef } from "./section/BasicInfoSection";
export { default as SupplierInfoSection } from "./section/SupplierInfoSection"; export { default as SupplierInfoSection } from "./section/SupplierInfoSection";
export { default as PurchaseCostInfoSection } from "./section/PurchaseCostInfoSection"; export { default as PurchaseCostInfoSection } from "./section/PurchaseCostInfoSection";
export { default as PackageInfoSection } from "./section/PackageInfoSection"; export { default as PackageInfoSection } from "./section/PackageInfoSection";
@ -44,6 +47,7 @@ export { default as PackagingCostSection } from "./section/PackagingCostSection"
export { default as ProductionLossSection } from "./section/ProductionLossSection"; export { default as ProductionLossSection } from "./section/ProductionLossSection";
export { default as CostSummarySection } from "./section/CostSummarySection"; export { default as CostSummarySection } from "./section/CostSummarySection";
export { default as MarketPriceSection } from "./section/MarketPriceSection"; export { default as MarketPriceSection } from "./section/MarketPriceSection";
export type { MarketPriceSectionRef } from "./section/MarketPriceSection";
export { default as RebateCalcSection } from "./section/RebateCalcSection"; export { default as RebateCalcSection } from "./section/RebateCalcSection";
export { default as TaxSubsidySection } from "./section/TaxSubsidySection"; export { default as TaxSubsidySection } from "./section/TaxSubsidySection";
export { default as TaxProvisionSection } from "./section/TaxProvisionSection"; export { default as TaxProvisionSection } from "./section/TaxProvisionSection";
@ -52,6 +56,7 @@ export { default as MaterialCostSection } from "./section/MaterialCostSection";
export { default as ProductionAdvanceSection } from "./section/ProductionAdvanceSection"; export { default as ProductionAdvanceSection } from "./section/ProductionAdvanceSection";
export { default as WorkerAdvanceSection } from "./section/WorkerAdvanceSection"; export { default as WorkerAdvanceSection } from "./section/WorkerAdvanceSection";
export { default as DeliveryFormSection } from "./section/DeliveryFormSection"; export { default as DeliveryFormSection } from "./section/DeliveryFormSection";
export type { DeliveryFormSectionRef } from "./section/DeliveryFormSection";
export { default as PurchaseFormSection } from "./section/PurchaseFormSection"; export { default as PurchaseFormSection } from "./section/PurchaseFormSection";
export { default as PurchaseStep1Form } from "./document/Step1Form"; export { default as PurchaseStep1Form } from "./document/Step1Form";

View File

@ -9,21 +9,45 @@ import {
SafeArea, SafeArea,
} from "@nutui/nutui-react-taro"; } from "@nutui/nutui-react-taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import businessServices from "@/services/business"; import businessServices from "@/services/business";
import { Icon } from "@/components"; import { Icon } from "@/components";
import { generateShortId } from "@/utils"; import { generateShortId } from "@/utils";
export default function BasicInfoSection(props: { export interface BasicInfoSectionRef {
validateForm: () => string | null;
}
export default forwardRef<
BasicInfoSectionRef,
{
orderVO: BusinessAPI.OrderVO; orderVO: BusinessAPI.OrderVO;
onChange?: (orderVO: BusinessAPI.OrderVO) => void; onChange?: (orderVO: BusinessAPI.OrderVO) => void;
costList: BusinessAPI.CostVO[]; costList: BusinessAPI.CostVO[];
readOnly?: boolean; readOnly?: boolean;
}) { }
>((props, ref) => {
const { orderVO, onChange, readOnly, costList } = props; const { orderVO, onChange, readOnly, costList } = props;
const { orderVehicle } = orderVO; const { orderVehicle } = orderVO;
// 暴露 validateForm 方法
useImperativeHandle(ref, () => ({
validateForm: () => {
// 校验本车次号
if (!orderVehicle?.vehicleNo) {
return "请输入本车次号";
}
// 校验运费类型
if (!orderVehicle?.priceType) {
return "请选择运费类型";
}
return null;
},
}));
// 当天和未来10天 // 当天和未来10天
const startDate = new Date(); const startDate = new Date();
const endDate = new Date(startDate.getTime() + 86400000 * 10); const endDate = new Date(startDate.getTime() + 86400000 * 10);
@ -809,4 +833,4 @@ export default function BasicInfoSection(props: {
</View> </View>
</> </>
); );
} });

View File

@ -1,15 +1,33 @@
import { useEffect, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import { CompanyPicker, Icon } from "@/components"; import { CompanyPicker, Icon } from "@/components";
import { Button } from "@nutui/nutui-react-taro"; import { Button } from "@nutui/nutui-react-taro";
export default function CompanyInfoSection(props: { export interface CompanyInfoSectionRef {
validateForm: () => string | null;
}
export default forwardRef<
CompanyInfoSectionRef,
{
orderVO: BusinessAPI.OrderVO; orderVO: BusinessAPI.OrderVO;
onChange?: (orderVO: BusinessAPI.OrderVO) => void; onChange?: (orderVO: BusinessAPI.OrderVO) => void;
readOnly?: boolean; readOnly?: boolean;
}) { }
>((props, ref) => {
const { orderVO, onChange, readOnly } = props; const { orderVO, onChange, readOnly } = props;
// 暴露 validateForm 方法
useImperativeHandle(ref, () => ({
validateForm: () => {
// 校验销售方
if (!orderVO?.orderCompany?.companyId) {
return "请选择销售方";
}
return null;
},
}));
const [orderCompany, setOrderCompany] = useState<BusinessAPI.OrderCompany>(); const [orderCompany, setOrderCompany] = useState<BusinessAPI.OrderCompany>();
// 当 orderVO 变化时,更新默认显示的公司信息 // 当 orderVO 变化时,更新默认显示的公司信息
@ -84,4 +102,4 @@ export default function CompanyInfoSection(props: {
)} )}
</View> </View>
); );
} });

View File

@ -1,15 +1,33 @@
import { useEffect, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import { DealerPicker, Icon } from "@/components"; import { DealerPicker, Icon } from "@/components";
import { Button } from "@nutui/nutui-react-taro"; import { Button } from "@nutui/nutui-react-taro";
export default function DealerInfoSection(props: { export interface DealerInfoSectionRef {
validateForm: () => string | null;
}
export default forwardRef<
DealerInfoSectionRef,
{
orderVO: BusinessAPI.OrderVO; orderVO: BusinessAPI.OrderVO;
onChange?: (orderDealer: BusinessAPI.OrderVO) => void; onChange?: (orderDealer: BusinessAPI.OrderVO) => void;
readOnly?: boolean; readOnly?: boolean;
}) { }
>((props, ref) => {
const { orderVO, onChange, readOnly } = props; const { orderVO, onChange, readOnly } = props;
// 暴露 validateForm 方法
useImperativeHandle(ref, () => ({
validateForm: () => {
// 校验经销商
if (!orderVO?.orderDealer?.dealerId) {
return "请选择经销商";
}
return null;
},
}));
const [orderDealer, setOrderDealer] = useState<BusinessAPI.OrderDealer>(); const [orderDealer, setOrderDealer] = useState<BusinessAPI.OrderDealer>();
// 添加经销商名称状态用于只有名称没有ID的情况 // 添加经销商名称状态用于只有名称没有ID的情况
@ -87,4 +105,4 @@ export default function DealerInfoSection(props: {
)} )}
</View> </View>
); );
} });

View File

@ -1,16 +1,34 @@
import { useEffect, useState } from "react"; import {
import { DeliveryStep1Form, DeliveryStep2Preview, Icon } from "@/components"; forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import {
DeliveryStep1Form,
DeliveryStep1FormRef,
DeliveryStep2Preview,
Icon,
} from "@/components";
import { convertOrderShipVOToExamplesFormat } from "@/utils"; import { convertOrderShipVOToExamplesFormat } from "@/utils";
import { business } from "@/services"; import { business } from "@/services";
import { Button, Popup } from "@nutui/nutui-react-taro"; import { Button, Popup } from "@nutui/nutui-react-taro";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
export default function DeliveryFormSection(props: { export interface DeliveryFormSectionRef {
validateForm: () => string | null;
}
export default forwardRef<
DeliveryFormSectionRef,
{
orderVO: BusinessAPI.OrderVO; orderVO: BusinessAPI.OrderVO;
onChange?: (orderDealer: BusinessAPI.OrderVO) => void; onChange?: (orderDealer: BusinessAPI.OrderVO) => void;
readOnly?: boolean; readOnly?: boolean;
costList: BusinessAPI.CostVO[]; costList: BusinessAPI.CostVO[];
}) { }
>((props, ref) => {
const { orderVO, onChange, readOnly, costList } = props; const { orderVO, onChange, readOnly, costList } = props;
const [template, setTemplate] = useState<any[]>([]); const [template, setTemplate] = useState<any[]>([]);
@ -90,6 +108,21 @@ export default function DeliveryFormSection(props: {
setPreviewVisible(true); setPreviewVisible(true);
}; };
const step1FormRef = useRef<DeliveryStep1FormRef>(null);
// 暴露 validateForm 方法
useImperativeHandle(ref, () => ({
validateForm: () => {
// 调用 step1FormRef 的 validateForm 方法
if (step1FormRef.current && step1FormRef.current.validateForm) {
if (!step1FormRef.current.validateForm()) {
return "发货单复核填写错误";
}
}
return null;
},
}));
if (!template) { if (!template) {
return; return;
} }
@ -99,6 +132,7 @@ export default function DeliveryFormSection(props: {
return ( return (
<> <>
<DeliveryStep1Form <DeliveryStep1Form
ref={step1FormRef}
readOnly={readOnly} readOnly={readOnly}
moduleList={moduleList} moduleList={moduleList}
orderShip={orderShip} orderShip={orderShip}
@ -146,4 +180,4 @@ export default function DeliveryFormSection(props: {
</Popup> </Popup>
</> </>
); );
} });

View File

@ -3,15 +3,42 @@ import { Radio } from "@nutui/nutui-react-taro";
import { OrderCalculator } from "@/utils"; import { OrderCalculator } from "@/utils";
import { Icon, PriceEditor } from "@/components"; import { Icon, PriceEditor } from "@/components";
import classNames from "classnames"; import classNames from "classnames";
import { forwardRef, useImperativeHandle } from "react";
export default function MarketPriceSection(props: { export interface MarketPriceSectionRef {
validateForm: () => string | null;
}
export default forwardRef<
MarketPriceSectionRef,
{
orderVO: BusinessAPI.OrderVO; orderVO: BusinessAPI.OrderVO;
onChange?: (orderVO: BusinessAPI.OrderVO) => void; onChange?: (orderVO: BusinessAPI.OrderVO) => void;
readOnly?: boolean; readOnly?: boolean;
calculator: OrderCalculator; calculator: OrderCalculator;
}) { }
>((props, ref) => {
const { orderVO, onChange, readOnly, calculator } = props; const { orderVO, onChange, readOnly, calculator } = props;
// 暴露 validateForm 方法
useImperativeHandle(ref, () => ({
validateForm: () => {
// 校验报价方式
if (!orderVO?.pricingMethod) {
return "请选择市场报价的报价方式";
}
// 校验销售单价
for (const supplier of orderVO.orderSupplierList || []) {
if (!supplier.salePrice || supplier.salePrice <= 0) {
return `请填写${supplier.name}的销售单价`;
}
}
return null;
},
}));
// 销售金额 // 销售金额
const saleAmount = calculator.getSalesAmount(); const saleAmount = calculator.getSalesAmount();
@ -242,4 +269,4 @@ export default function MarketPriceSection(props: {
</View> </View>
</> </>
); );
} });

View File

@ -1,2 +1,2 @@
// App 相关常量 // App 相关常量
export const APP_VERSION = "v0.0.64"; export const APP_VERSION = "v0.0.65";

View File

@ -260,7 +260,8 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderDealer?.taxProvision && ( {orderVO.orderDealer?.taxProvision &&
orderVO.orderDealer?.taxProvision > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -269,7 +270,8 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderDealer?.taxSubsidy && ( {orderVO.orderDealer?.taxSubsidy &&
orderVO.orderDealer?.taxSubsidy > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -278,7 +280,7 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderRebate?.amount && ( {orderVO.orderRebate?.amount && orderVO.orderRebate?.amount > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -287,7 +289,8 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderDealer?.costDifference && ( {orderVO.orderDealer?.costDifference &&
orderVO.orderDealer?.costDifference > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">

View File

@ -2,7 +2,7 @@ import hocAuth from "@/hocs/auth";
import { CommonComponent } from "@/types/typings"; import { CommonComponent } from "@/types/typings";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { business } from "@/services"; import { business } from "@/services";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import { import {
ActionSheet, ActionSheet,
@ -14,14 +14,19 @@ import {
} from "@nutui/nutui-react-taro"; } from "@nutui/nutui-react-taro";
import { import {
BasicInfoSection, BasicInfoSection,
BasicInfoSectionRef,
CompanyInfoSection, CompanyInfoSection,
CompanyInfoSectionRef,
CostDifferenceSection, CostDifferenceSection,
CostSummarySection, CostSummarySection,
DealerInfoSection, DealerInfoSection,
DealerInfoSectionRef,
DeliveryFormSection, DeliveryFormSection,
DeliveryFormSectionRef,
EmptyBoxInfoSection, EmptyBoxInfoSection,
Icon, Icon,
MarketPriceSection, MarketPriceSection,
MarketPriceSectionRef,
MaterialCostSection, MaterialCostSection,
OrderRejectApprove, OrderRejectApprove,
PackageInfoSection, PackageInfoSection,
@ -47,6 +52,15 @@ import classNames from "classnames";
import { DecimalUtils } from "@/utils/classes/calculators/core/DecimalUtils"; import { DecimalUtils } from "@/utils/classes/calculators/core/DecimalUtils";
import order from "@/constant/order"; import order from "@/constant/order";
// Section ref 类型
type SectionRef =
| BasicInfoSectionRef
| CompanyInfoSectionRef
| DealerInfoSectionRef
| MarketPriceSectionRef
| DeliveryFormSectionRef
| null;
const defaultSections = [ const defaultSections = [
"marketPrice", "marketPrice",
"supplierInfo", "supplierInfo",
@ -234,6 +248,9 @@ export default hocAuth(function Page(props: CommonComponent) {
const orderId = router.params.orderId as BusinessAPI.OrderVO["orderId"]; const orderId = router.params.orderId as BusinessAPI.OrderVO["orderId"];
const auditId = router.params.auditId as BusinessAPI.AuditVO["auditId"]; const auditId = router.params.auditId as BusinessAPI.AuditVO["auditId"];
// Section refs 映射
const sectionRefs = useRef<Record<string, SectionRef>>({});
// 费用项目列表 // 费用项目列表
const [costList, setCostList] = useState<BusinessAPI.CostVO[]>([]); const [costList, setCostList] = useState<BusinessAPI.CostVO[]>([]);
@ -313,13 +330,24 @@ export default hocAuth(function Page(props: CommonComponent) {
// 关闭对话框 // 关闭对话框
setSubmitDialogVisible(false); setSubmitDialogVisible(false);
// 表单校验 // 先调用所有 section 的 validateForm 方法进行校验
const errorMsg = validateForm(); const sectionErrors: string[] = [];
if (errorMsg) { Object.entries(sectionRefs.current).forEach(
([_sectionName, sectionRef]) => {
if (sectionRef && sectionRef.validateForm) {
const error = sectionRef.validateForm();
if (error) {
sectionErrors.push(error);
}
}
},
);
if (sectionErrors.length > 0) {
Toast.show("toast", { Toast.show("toast", {
icon: "fail", icon: "fail",
title: "校验失败", title: "校验失败",
content: errorMsg, content: sectionErrors[0],
}); });
return; return;
} }
@ -435,43 +463,6 @@ export default hocAuth(function Page(props: CommonComponent) {
} }
}; };
// 表单校验
const validateForm = () => {
// 校验销售方
if (!orderVO?.orderCompany?.companyId) {
return "请选择销售方";
}
// 校验经销商
if (!orderVO?.orderDealer?.dealerId) {
return "请选择经销商";
}
// 校验本车次号
if (!orderVO?.orderVehicle?.vehicleNo) {
return "请输入本车次号";
}
// 校验运费类型
if (!orderVO?.orderVehicle?.priceType) {
return "请选择运费类型";
}
// 校验市场报价的报价方式
if (!orderVO?.pricingMethod) {
return "请选择市场报价的报价方式";
}
// 校验市场报价的销售单价
orderVO.orderSupplierList.forEach((supplier: BusinessAPI.OrderSupplier) => {
if (!supplier.salePrice || supplier.salePrice <= 0) {
return "请填写市场报价的销售单价";
}
});
return null;
};
const init = async ( const init = async (
orderId: BusinessAPI.OrderVO["orderId"], orderId: BusinessAPI.OrderVO["orderId"],
auditId: BusinessAPI.AuditVO["auditId"], auditId: BusinessAPI.AuditVO["auditId"],
@ -824,6 +815,11 @@ export default hocAuth(function Page(props: CommonComponent) {
className={`overflow-x-auto rounded-md rounded-b-lg bg-white p-2.5 shadow-sm`} className={`overflow-x-auto rounded-md rounded-b-lg bg-white p-2.5 shadow-sm`}
> >
<section.component <section.component
ref={(ref: SectionRef) => {
if (ref) {
sectionRefs.current[section.name] = ref;
}
}}
readOnly={ readOnly={
!( !(
orderVO.state === "AUDITING" && orderVO.state === "AUDITING" &&