feat(purchase): 添加拼车选择功能并优化UI细节

- 在MelonFarmer组件中新增supplierCount属性用于判断是否显示拼车选项
- 修改"是否为最后一个瓜农"为"是否要拼车"的逻辑与文案
- 更新多个图标引用,包括新增address-book图标
- 调整输入框样式,增加图标前缀提升用户体验
- 优化称重信息页面的提示文字,使其更清晰易懂
- 增加OrderPackage相关类型定义及转换工具函数
- 更新页面审核和创建流程中的样式与交互逻辑
- 升级iconfont字体文件版本,支持新图标
- 修复部分组件样式问题,如SupplierList底部间距等
This commit is contained in:
shenyifei 2025-11-04 22:36:45 +08:00
parent fb7951e2f3
commit 1ac1564ec2
12 changed files with 1587 additions and 732 deletions

View File

@ -3,6 +3,7 @@ import classNames from "classnames";
import React from "react";
export type IconNames =
| "address-book"
| "pen-to-square"
| "location-dot"
| "clock"

View File

@ -25,11 +25,12 @@ interface IMelonFarmerProps {
onRemove: (supplierVO: SupplierVO) => void;
onAdd: () => void;
isLast: boolean; // 添加一个属性来标识是否是最后一个瓜农
supplierCount: number;
}
export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
function MelonFarmer(props, ref) {
const { value, onChange, onRemove, onAdd, isLast } = props;
const { value, onChange, onRemove, onAdd, isLast, supplierCount } = props;
const [supplierVO, setSupplierVO] = useState<SupplierVO>();
console.log("supplierVO", supplierVO);
@ -94,7 +95,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
);
const [isLastFarmerError, setIsLastFarmerError] = useState<{
[key: string]: boolean;
}>({}); // 添加是否为最后一个瓜农的错误状态
}>({}); // 添加是否要拼车的错误状态
// 校验姓名函数
const validateName = (name: string) => {
@ -136,9 +137,10 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
return phoneRegex.test(phone);
};
// 校验是否为最后一个瓜农函数
// 校验是否要拼车函数
// 校验是否要拼车函数
const validateIsLastFarmer = (isLast?: boolean) => {
// 必须选择是或否
// 必须选择要不要
return isLast !== undefined;
};
@ -151,7 +153,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
const isIdCardValid = validateIdCard(supplierVO.idCard || "");
const isBankCardValid = validateBankCard(supplierVO.bankCard || "");
const isPhoneValid = validatePhone(supplierVO.phone || "");
const isLastFarmerValid = validateIsLastFarmer(supplierVO?.isLast); // 校验是否为最后一个瓜农
const isLastFarmerValid = validateIsLastFarmer(supplierVO?.isLast); // 校验是否需要拼车
// 更新错误状态
setNameError((prev) => ({
@ -384,7 +386,13 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
}
return (
<View className="flex flex-1 flex-col gap-2.5 p-2.5">
<View
className="flex flex-1 flex-col gap-2.5 p-2.5"
style={{
//@ts-ignore
"--nutui-input-padding": 0,
}}
>
{/* 功能提醒 */}
<View className="flex items-center rounded-lg border border-blue-200 bg-blue-50 p-2.5">
<Icon
@ -398,6 +406,61 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
</View>
</View>
{/* 只有最后一个瓜农才显示是否为最后一个瓜农的选项和添加按钮 */}
{isLast && (
<View className="rounded-lg bg-white p-2.5 shadow-sm">
<View className="flex items-center justify-between">
{supplierCount > 1 ? (
<View className="text-sm"></View>
) : (
<View className="text-sm"></View>
)}
<View className="text-neutral-darkest text-sm font-medium">
<Radio.Group
direction="horizontal"
value={
supplierVO.isLast === true
? "true"
: supplierVO.isLast === false
? "false"
: undefined
}
onChange={(value) => {
// 清除错误状态
setIsLastFarmerError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: false,
}));
// 根据用户选择设置是否为最后一个瓜农
const isLastValue =
value === "true"
? true
: value === "false"
? false
: undefined;
setSupplierVO({
...supplierVO,
isLast: isLastValue,
});
setIsLastFarmer(isLastValue!);
}}
>
<Radio value="false"></Radio>
<Radio value="true"></Radio>
</Radio.Group>
</View>
</View>
{isLastFarmerError[supplierVO.orderSupplierId] && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
)}
{/* 快捷工具 */}
<View className="flex gap-2">
<View className={"flex-1"}>
@ -477,15 +540,22 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
</View>
</View>
<View className="mb-2.5">
<View className="mb-1 flex flex-row">
<View className="mb-1 flex flex-row justify-between">
<View className={"block text-sm font-normal text-[#000000]"}>
</View>
<SupplierPicker
trigger={
<View className="flex items-center">
<Icon
name="address-book"
size={16}
color="var(--color-primary)"
/>
<Text className={"text-primary ml-1 text-sm font-bold"}>
</Text>
</View>
}
onFinish={(supplierVO1) => {
setSupplierVO({
@ -516,6 +586,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
<View
className={`flex h-10 w-full items-center rounded-md ${nameError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon name="user" size={16} color="#999" className="mx-2" />
<Input
type="text"
placeholder="请输入姓名"
@ -527,6 +598,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
supplierVO.orderSupplierId,
)
}
className="flex-1"
/>
</View>
{nameError[supplierVO.orderSupplierId] && (
@ -540,6 +612,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
<View
className={`flex h-10 w-full items-center rounded-md ${idCardError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon name="id-card" size={16} color="#999" className="mx-2" />
<Input
type="text"
placeholder="请输入身份证号"
@ -551,6 +624,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
supplierVO.orderSupplierId,
)
}
className="flex-1"
/>
</View>
{idCardError[supplierVO.orderSupplierId] && (
@ -566,6 +640,12 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
<View
className={`flex h-10 w-full items-center rounded-md ${bankCardError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon
name="credit-card"
size={16}
color="#999"
className="mx-2"
/>
<Input
type="text"
placeholder="请输入银行卡号"
@ -577,6 +657,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
supplierVO.orderSupplierId,
)
}
className="flex-1"
/>
</View>
{bankCardError[supplierVO.orderSupplierId] && (
@ -592,6 +673,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
<View
className={`flex h-10 w-full items-center rounded-md ${phoneError[supplierVO.orderSupplierId] ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon name="phone" size={16} color="#999" className="mx-2" />
<Input
type="tel"
placeholder="请输入手机号码"
@ -603,6 +685,7 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
supplierVO.orderSupplierId,
)
}
className="flex-1"
/>
</View>
{phoneError[supplierVO.orderSupplierId] && (
@ -639,56 +722,6 @@ export default forwardRef<MelonFarmerRef, IMelonFarmerProps>(
</View>
</View>
{/* 只有最后一个瓜农才显示是否为最后一个瓜农的选项和添加按钮 */}
{isLast && (
<View className="rounded-lg bg-white p-2.5 shadow-sm">
<View className="flex items-center justify-between">
<View className="text-sm"></View>
<View className="text-neutral-darkest text-sm font-medium">
<Radio.Group
direction="horizontal"
value={
supplierVO.isLast === true
? "true"
: supplierVO.isLast === false
? "false"
: undefined
}
onChange={(value) => {
// 清除错误状态
setIsLastFarmerError((prev) => ({
...prev,
[supplierVO.orderSupplierId]: false,
}));
// 根据用户选择设置是否为最后一个瓜农
const isLastValue =
value === "true"
? true
: value === "false"
? false
: undefined;
setSupplierVO({
...supplierVO,
isLast: isLastValue,
});
setIsLastFarmer(isLastValue!);
}}
>
<Radio value="true"></Radio>
<Radio value="false"></Radio>
</Radio.Group>
</View>
</View>
{isLastFarmerError[supplierVO.orderSupplierId] && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
)}
{/* 只有当用户选择"否"时才显示添加按钮 */}
{isLast && !isLastFarmer && isLastFarmer !== null && (
<Button

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ export default function SupplierList(props: ISupplierListProps) {
console.log("value", value);
return (
<View className={"sticky top-12 z-10 bg-[#D1D5DB] p-2.5"}>
<View className={"sticky top-12 z-10 bg-[#D1D5DB] p-2.5 pb-0"}>
<ScrollView className={"rounded-xl bg-white shadow-sm"} scrollX>
<View className="flex flex-row">
{value.map((supplierVO: SupplierVO) => (

View File

@ -265,18 +265,8 @@ export default forwardRef<WeighRef, IWeightProps>(function Weigh(props, ref) {
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="text-primary mb-2.5 text-base font-bold">
{supplierVO.name}
</View>
<View className="mb-2.5">
<View className="mb-2.5">
<View
className={
"flex flex-row items-center justify-between rounded-lg bg-gray-50 p-2.5"
}
>
<View className="text-sm"></View>
<View className={"flex items-center justify-between gap-2.5"}>
<View className="text-sm"></View>
<View className="text-neutral-darkest text-sm font-medium">
<Radio.Group
direction="horizontal"
@ -307,18 +297,24 @@ export default forwardRef<WeighRef, IWeightProps>(function Weigh(props, ref) {
});
}}
>
<Radio value="true"></Radio>
<Radio value="false"></Radio>
<Radio value="true"></Radio>
<Radio value="false"></Radio>
</Radio.Group>
</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">

View File

@ -37,7 +37,7 @@ export default function SupplierPicker(props: ISupplierPickerProps) {
};
return (
<View className={"flex-1"}>
<View>
<View
onClick={(event) => {
setVisible(true);

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 5042354 */
src: url('//at.alicdn.com/t/c/font_5042354_lmz58v86a1h.woff2?t=1761549950025') format('woff2'),
url('//at.alicdn.com/t/c/font_5042354_lmz58v86a1h.woff?t=1761549950025') format('woff'),
url('//at.alicdn.com/t/c/font_5042354_lmz58v86a1h.ttf?t=1761549950025') format('truetype');
src: url('//at.alicdn.com/t/c/font_5042354_wqti51yo9xk.woff2?t=1762227792781') format('woff2'),
url('//at.alicdn.com/t/c/font_5042354_wqti51yo9xk.woff?t=1762227792781') format('woff'),
url('//at.alicdn.com/t/c/font_5042354_wqti51yo9xk.ttf?t=1762227792781') format('truetype');
}
.iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-address-book:before {
content: "\e623";
}
.icon-pen-to-square:before {
content: "\e630";
}

View File

@ -70,67 +70,67 @@ const sectionList = {
const sectionConfig = {
supplierInfo: {
title: "销售方信息",
containerClass: "border-l-4 border-l-blue-500",
containerClass: "border-l-8 border-l-blue-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
dealerInfo: {
title: "下游经销商信息",
containerClass: "border-l-4 border-l-green-500",
containerClass: "border-l-8 border-l-green-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
basicInfo: {
title: "基础信息",
containerClass: "border-l-4 border-l-yellow-500",
containerClass: "border-l-8 border-l-yellow-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
farmerInfo: {
title: "瓜农信息",
containerClass: "border-l-4 border-l-purple-500",
containerClass: "border-l-8 border-l-purple-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
purchaseCostInfo: {
title: "采购成本",
containerClass: "border-l-4 border-l-red-500",
containerClass: "border-l-8 border-l-red-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
packageInfo: {
title: "包装纸箱费",
containerClass: "border-l-4 border-l-indigo-500",
containerClass: "border-l-8 border-l-indigo-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
emptyBoxInfo: {
title: "空箱费用",
containerClass: "border-l-4 border-l-pink-500",
containerClass: "border-l-8 border-l-pink-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
laborInfo: {
title: "用工信息",
containerClass: "border-l-4 border-l-teal-500",
containerClass: "border-l-8 border-l-teal-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
packagingCost: {
title: "包装费",
containerClass: "border-l-4 border-l-orange-500",
containerClass: "border-l-8 border-l-orange-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
costSummary: {
title: "成本合计",
containerClass: "border-l-4 border-l-cyan-500",
containerClass: "border-l-8 border-l-cyan-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
marketPrice: {
title: "市场报价",
containerClass: "border-l-4 border-l-lime-500",
containerClass: "border-l-8 border-l-lime-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
rebateCalc: {
title: "返点计算",
containerClass: "border-l-4 border-l-amber-500",
containerClass: "border-l-8 border-l-amber-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
profitCalc: {
title: "利润计算",
containerClass: "border-l-4 border-l-emerald-500",
containerClass: "border-l-8 border-l-emerald-500",
contentClass: "p-4 bg-white rounded-b-lg shadow-sm overflow-x-auto",
},
};

View File

@ -22,6 +22,7 @@ import { business } from "@/services";
import { generateShortId } from "@/utils/generateShortId";
import Taro from "@tarojs/taro";
import buildUrl from "@/utils/buildUrl";
import { OrderPackageRef } from "@/components/purchase/OrderPackage";
const defaultSupplierList: SupplierVO[] = [
{
@ -62,8 +63,10 @@ export default hocAuth(function Page(props: CommonComponent) {
const melonFarmerRefs = useRef<MelonFarmerRef[]>([]);
// 创建Weigh组件的ref数组
const weighRefs = useRef<WeighRef[]>([]);
// 创建Artificial组件的ref
const artificialRef = useRef<OrderCostRef>(null);
// 创建OrderCost组件的ref
const orderCostRef = useRef<OrderCostRef>(null);
// 创建OrderPackage组件的ref
const orderPackageRefs = useRef<OrderPackageRef[]>([]);
const [purchaseOrder, setPurchaseOrder] =
useState<BusinessAPI.PurchaseOrderCreateCmd>();
@ -355,6 +358,7 @@ export default hocAuth(function Page(props: CommonComponent) {
return (
<MelonFarmer
supplierCount={orderSupplierList.length}
key={item.orderSupplierId}
ref={(ref) => {
if (ref) {
@ -442,9 +446,21 @@ export default hocAuth(function Page(props: CommonComponent) {
// 包装信息
if (step.value === 4) {
// 确保ref数组足够长
while (orderPackageRefs.current.length <= index) {
orderPackageRefs.current.push({
validate: () => true, // 默认验证方法
} as OrderPackageRef);
}
return (
<OrderPackage
key={item.orderSupplierId}
ref={(ref) => {
if (ref) {
orderPackageRefs.current[index] = ref;
}
}}
value={item}
onChange={(supplierVO: SupplierVO) => {
orderSupplierList[index] = { ...item, ...supplierVO };
@ -472,7 +488,7 @@ export default hocAuth(function Page(props: CommonComponent) {
{/* 人工和辅料信息 */}
{step.value === 6 && (
<OrderCost
ref={artificialRef}
ref={orderCostRef}
value={orderCostList}
onChange={(costItemList: CostItem[]) =>
setOrderCostList(costItemList)
@ -553,6 +569,18 @@ export default hocAuth(function Page(props: CommonComponent) {
) {
setActive(active + 1);
}
} // 在第四步(包装信息)时进行校验
else if (active === 4) {
// 获取当前选中的供应商
const selectedIndex = orderSupplierList.findIndex(
(supplier) => supplier.selected,
);
if (
selectedIndex !== -1 &&
orderPackageRefs.current[selectedIndex]?.validate()
) {
setActive(active + 1);
}
} else {
setActive(active + 1);
}
@ -572,7 +600,7 @@ export default hocAuth(function Page(props: CommonComponent) {
className="btn-large bg-primary ml-2 flex-1 text-white"
onClick={async () => {
// 第六步(人工辅料)时进行校验
if (artificialRef.current?.validate()) {
if (orderCostRef.current?.validate()) {
await onFinish(true);
}
}}

View File

@ -2165,6 +2165,7 @@ declare namespace BusinessAPI {
boxBrandId: string;
/** 箱子品牌名称 */
boxBrandName: string;
boxBrandImage: string;
/** 箱子分类ID */
boxCategoryId: string;
/** 箱子产品ID */
@ -2180,7 +2181,7 @@ declare namespace BusinessAPI {
/** 销售单价(元/个) */
boxSalePrice?: number;
/** 箱子类型:1_本次使用2_额外运输3_已使用额外运输4_车上剩余 */
boxType: "USED" | "EXTRA" | "EXTRA_USED" | "REMAIN";
boxType: "USED" | "EXTRA" | "EXTRA_USED" | "REMAIN" | "OWN" | "DEFAULT";
};
type OrderRebate = {

View File

@ -32,14 +32,18 @@ export interface BoxBrand {
id: string;
boxBrandId: string;
boxBrandName: string;
boxBrandImage: string | undefined;
boxBrandImage: string;
boxType: "USED" | "EXTRA" | "EXTRA_USED" | "REMAIN" | "OWN" | "DEFAULT";
boxCategoryList: BoxCategory[];
}
export interface BoxProduct {
id: string;
boxBrandId: string;
boxBrandName: string;
boxProductId: string;
boxProductName: string;
boxCount: number;
boxProductWeight: number;
boxCategoryId: "FOUR_GRAIN" | "TWO_GRAIN";
boxCostPrice: number;

View File

@ -0,0 +1,112 @@
// 将BoxBrand转换为OrderPackage数组
import { BoxBrand, BoxCategory, BoxProduct } from "@/types/typings";
import { generateShortId } from "@/utils/generateShortId";
// 添加一个辅助函数用于分组
const groupBy = <T,>(
array: T[],
keyFn: (item: T) => string,
): Record<string, T[]> => {
return array.reduce(
(groups, item) => {
const key = keyFn(item);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
},
{} as Record<string, T[]>,
);
};
export const convertBoxBrandToOrderPackages = (
boxBrand: BoxBrand,
boxType: BusinessAPI.OrderPackage["boxType"] = "USED"
): BusinessAPI.OrderPackage[] => {
const orderPackages: BusinessAPI.OrderPackage[] = [];
boxBrand.boxCategoryList?.forEach((category) => {
category.boxProductList.forEach((product) => {
orderPackages.push({
orderPackageId: product.id,
boxBrandId: product.boxBrandId,
boxBrandName: product.boxBrandName,
boxBrandImage: boxBrand.boxBrandImage,
boxCategoryId: product.boxCategoryId,
boxProductId: product.boxProductId,
boxProductName: product.boxProductName,
boxProductWeight: product.boxProductWeight,
boxCount: product.boxCount || 0,
boxCostPrice: product.boxCostPrice,
boxSalePrice: product.boxSalePrice,
boxType: boxType,
});
});
});
return orderPackages;
};
// 将OrderPackage数组转换为BoxBrand数组
export const convertOrderPackagesToBoxBrands = (
orderPackages: BusinessAPI.OrderPackage[]
): BoxBrand[] => {
// 按品牌ID分组
const packagesByBrand = groupBy(
orderPackages,
(pkg) => pkg.boxBrandId
);
// 转换为BoxBrand数组
return Object.entries(packagesByBrand).map(([brandId, packages]) => {
const firstPackage = packages[0];
// 按分类ID分组
const packagesByCategory = groupBy(
packages,
(pkg) => pkg.boxCategoryId
);
// 创建BoxCategory数组
const boxCategories: BoxCategory[] = Object.entries(packagesByCategory).map(
([categoryId, categoryPackages]) => {
const firstCategoryPackage = categoryPackages[0];
// 创建BoxProduct数组
const boxProducts: BoxProduct[] = categoryPackages.map((pkg) => ({
id: pkg.orderPackageId || generateShortId(),
boxBrandId: pkg.boxBrandId,
boxBrandName: pkg.boxBrandName,
boxProductId: pkg.boxProductId,
boxProductName: pkg.boxProductName,
boxProductWeight: pkg.boxProductWeight,
boxCategoryId: pkg.boxCategoryId,
boxCostPrice: pkg.boxCostPrice,
boxSalePrice: pkg.boxSalePrice,
boxCount: pkg.boxCount,
} as BoxProduct));
return {
id: generateShortId(),
boxCategoryId: categoryId as "FOUR_GRAIN" | "TWO_GRAIN",
boxCategoryName: firstCategoryPackage.boxCategoryId === "FOUR_GRAIN"
? "4粒装"
: firstCategoryPackage.boxCategoryId === "TWO_GRAIN"
? "2粒装"
: "未知",
boxProductList: boxProducts,
};
}
);
return {
id: brandId,
boxBrandId: brandId,
boxType: firstPackage.boxType,
boxBrandName: firstPackage.boxBrandName,
boxBrandImage: firstPackage.boxBrandImage,
boxCategoryList: boxCategories,
};
});
};