feat(components): 添加图片预览组件并重构交付单预览功能
- 新增 ImagePreview 组件支持图片缩放和拖拽功能 - 将 Step2Preview 中的预览逻辑提取到 ImagePreview 组件 - 移除 Step3Success 中的重复利润计算代码 - 添加利润明细表预览功能到成本差异模块 - 优化上传组件支持 PDF 文件类型显示 - 修复 OCR 相机页面闪关灯默认状态问题 - 更新供应商信息识别结果字段映射 - 调整版本号至 v0.0.75
This commit is contained in:
parent
0742491943
commit
e3b6f056b8
139
packages/app-client/src/components/biz/ImagePreview.tsx
Normal file
139
packages/app-client/src/components/biz/ImagePreview.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import React, { useState } from "react";
|
||||
import { Text, View } from "@tarojs/components";
|
||||
import { Button } from "@nutui/nutui-react-taro";
|
||||
|
||||
interface IImagePreview {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ImagePreview(props: IImagePreview) {
|
||||
const { children } = props;
|
||||
|
||||
const [scale, setScale] = useState(0.5); // 缩放比例
|
||||
const [touchInfo, setTouchInfo] = useState({
|
||||
startDistance: 0,
|
||||
startScale: 0.5,
|
||||
}); // 触摸信息
|
||||
const [position, setPosition] = useState({ x: 0, y: -(29.7 * 37.8 * 0.5) }); // 当前位置
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); // 起始位置
|
||||
|
||||
// 放大
|
||||
const zoomIn = () => {
|
||||
setScale((prev) => Math.min(prev + 0.1, 3)); // 最大放大到3倍
|
||||
};
|
||||
|
||||
// 缩小
|
||||
const zoomOut = () => {
|
||||
setScale((prev) => Math.max(prev - 0.1, 0.5)); // 最小缩小到0.5倍
|
||||
};
|
||||
|
||||
// 重置缩放
|
||||
const resetZoom = () => {
|
||||
setScale(0.5);
|
||||
setPosition({ x: 0, y: -(29.7 * 37.8 * 0.5) }); // 重置位置
|
||||
};
|
||||
|
||||
// 计算两点间距离
|
||||
const getDistance = (touches: any) => {
|
||||
const dx = touches[0].clientX - touches[1].clientX;
|
||||
const dy = touches[0].clientY - touches[1].clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
// 处理触摸开始事件
|
||||
const handleTouchStart = (e: any) => {
|
||||
const touches = e.touches;
|
||||
if (touches.length === 2) {
|
||||
// 双指触摸,记录初始距离和当前缩放值
|
||||
const distance = getDistance(touches);
|
||||
setTouchInfo({
|
||||
startDistance: distance,
|
||||
startScale: scale,
|
||||
});
|
||||
} else if (touches.length === 1) {
|
||||
// 单指触摸,记录起始位置
|
||||
setStartPosition({
|
||||
x: touches[0].clientX - position.x,
|
||||
y: touches[0].clientY - position.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理触摸移动事件
|
||||
const handleTouchMove = (e: any) => {
|
||||
const touches = e.touches;
|
||||
e.preventDefault(); // 阻止默认滚动行为
|
||||
|
||||
if (touches.length === 2) {
|
||||
// 双指触摸,计算缩放比例
|
||||
const currentDistance = getDistance(touches);
|
||||
const newScale =
|
||||
touchInfo.startScale * (currentDistance / touchInfo.startDistance);
|
||||
// 限制缩放范围在0.5到3之间
|
||||
setScale(Math.min(Math.max(0.5, newScale), 3));
|
||||
} else if (touches.length === 1 && scale > 0.5) {
|
||||
// 单指触摸且已放大,允许拖动
|
||||
setPosition({
|
||||
x: touches[0].clientX - startPosition.x,
|
||||
y: touches[0].clientY - startPosition.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理触摸结束事件
|
||||
const handleTouchEnd = (e: any) => {
|
||||
// 可以在这里添加触摸结束后的处理逻辑
|
||||
console.log("Touch ended", e);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex flex-1 flex-col items-center overflow-hidden">
|
||||
{/* 缩放控制按钮 */}
|
||||
<View className="mb-2 flex w-full justify-center gap-2 bg-white p-2">
|
||||
<Button size="small" onClick={zoomOut}>
|
||||
-
|
||||
</Button>
|
||||
<Button size="small" onClick={resetZoom}>
|
||||
重置
|
||||
</Button>
|
||||
<Button size="small" onClick={zoomIn}>
|
||||
+
|
||||
</Button>
|
||||
<Text className="ml-2 flex items-center">
|
||||
{Math.round(scale * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 预览区域 */}
|
||||
<View
|
||||
className="flex-1 overflow-auto"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View
|
||||
id="preview"
|
||||
className="rounded-lg bg-white shadow-md"
|
||||
style={{
|
||||
width: "21cm",
|
||||
padding: "2cm 1cm 0 1cm",
|
||||
margin: "0 auto",
|
||||
height: "29.7cm", // A4纸高度
|
||||
transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)`,
|
||||
transformOrigin: "center center",
|
||||
transition:
|
||||
scale === touchInfo.startScale ? "transform 0.2s ease" : "none",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@ -10,6 +10,7 @@ export { CustomTheme } from "./CustomTheme";
|
||||
export { TabBar } from "./TabBar";
|
||||
export { default as PriceEditor } from "./PriceEditor";
|
||||
export { default as VersionChecker } from "./VersionChecker";
|
||||
export { default as ImagePreview } from "./ImagePreview";
|
||||
export type { TabBarProps } from "./TabBar";
|
||||
|
||||
export * from "./typing";
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { Text, View } from "@tarojs/components";
|
||||
import { Button } from "@nutui/nutui-react-taro";
|
||||
import { ImagePreview } from "@/components";
|
||||
import {
|
||||
DealerInfo,
|
||||
OtherFees,
|
||||
@ -20,167 +18,45 @@ interface Step2PreviewProps {
|
||||
export default function Step2Preview(props: Step2PreviewProps) {
|
||||
const { moduleList } = props;
|
||||
|
||||
const [scale, setScale] = useState(0.5); // 缩放比例
|
||||
const [touchInfo, setTouchInfo] = useState({
|
||||
startDistance: 0,
|
||||
startScale: 0.5,
|
||||
}); // 触摸信息
|
||||
const [position, setPosition] = useState({ x: 0, y: -(29.7 * 37.8 * 0.5) }); // 当前位置
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); // 起始位置
|
||||
|
||||
// 放大
|
||||
const zoomIn = () => {
|
||||
setScale((prev) => Math.min(prev + 0.1, 3)); // 最大放大到3倍
|
||||
};
|
||||
|
||||
// 缩小
|
||||
const zoomOut = () => {
|
||||
setScale((prev) => Math.max(prev - 0.1, 0.5)); // 最小缩小到0.5倍
|
||||
};
|
||||
|
||||
// 重置缩放
|
||||
const resetZoom = () => {
|
||||
setScale(0.5);
|
||||
setPosition({ x: 0, y: -(29.7 * 37.8 * 0.5) }); // 重置位置
|
||||
};
|
||||
|
||||
// 计算两点间距离
|
||||
const getDistance = (touches: any) => {
|
||||
const dx = touches[0].clientX - touches[1].clientX;
|
||||
const dy = touches[0].clientY - touches[1].clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
// 处理触摸开始事件
|
||||
const handleTouchStart = (e: any) => {
|
||||
const touches = e.touches;
|
||||
if (touches.length === 2) {
|
||||
// 双指触摸,记录初始距离和当前缩放值
|
||||
const distance = getDistance(touches);
|
||||
setTouchInfo({
|
||||
startDistance: distance,
|
||||
startScale: scale,
|
||||
});
|
||||
} else if (touches.length === 1) {
|
||||
// 单指触摸,记录起始位置
|
||||
setStartPosition({
|
||||
x: touches[0].clientX - position.x,
|
||||
y: touches[0].clientY - position.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理触摸移动事件
|
||||
const handleTouchMove = (e: any) => {
|
||||
const touches = e.touches;
|
||||
e.preventDefault(); // 阻止默认滚动行为
|
||||
|
||||
if (touches.length === 2) {
|
||||
// 双指触摸,计算缩放比例
|
||||
const currentDistance = getDistance(touches);
|
||||
const newScale =
|
||||
touchInfo.startScale * (currentDistance / touchInfo.startDistance);
|
||||
// 限制缩放范围在0.5到3之间
|
||||
setScale(Math.min(Math.max(0.5, newScale), 3));
|
||||
} else if (touches.length === 1 && scale > 0.5) {
|
||||
// 单指触摸且已放大,允许拖动
|
||||
setPosition({
|
||||
x: touches[0].clientX - startPosition.x,
|
||||
y: touches[0].clientY - startPosition.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理触摸结束事件
|
||||
const handleTouchEnd = (e: any) => {
|
||||
// 可以在这里添加触摸结束后的处理逻辑
|
||||
console.log("Touch ended", e);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex flex-1 flex-col items-center overflow-hidden">
|
||||
{/* 缩放控制按钮 */}
|
||||
<View className="mb-2 flex w-full justify-center gap-2 bg-white p-2">
|
||||
<Button size="small" onClick={zoomOut}>
|
||||
-
|
||||
</Button>
|
||||
<Button size="small" onClick={resetZoom}>
|
||||
重置
|
||||
</Button>
|
||||
<Button size="small" onClick={zoomIn}>
|
||||
+
|
||||
</Button>
|
||||
<Text className="ml-2 flex items-center">
|
||||
{Math.round(scale * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
<ImagePreview>
|
||||
{moduleList.map((module) => {
|
||||
if (module.type === "title") {
|
||||
return <TitleInfo key={"title"} module={module} />;
|
||||
}
|
||||
|
||||
{/* 预览区域 */}
|
||||
<View
|
||||
className="flex-1 overflow-auto"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View
|
||||
id="preview"
|
||||
className="rounded-lg bg-white shadow-md"
|
||||
style={{
|
||||
width: "21cm",
|
||||
padding: "2cm 1cm 0 1cm",
|
||||
margin: "0 auto",
|
||||
height: "29.7cm", // A4纸高度
|
||||
transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)`,
|
||||
transformOrigin: "center center",
|
||||
transition:
|
||||
scale === touchInfo.startScale ? "transform 0.2s ease" : "none",
|
||||
}}
|
||||
>
|
||||
{moduleList.map((module) => {
|
||||
if (module.type === "title") {
|
||||
return <TitleInfo key={"title"} module={module} />;
|
||||
}
|
||||
if (module.type === "dealerInfo") {
|
||||
return <DealerInfo key={"dealerInfo"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "dealerInfo") {
|
||||
return <DealerInfo key={"dealerInfo"} module={module} />;
|
||||
}
|
||||
if (module.type === "shippingInfo") {
|
||||
return <ShippingInfo key={"shippingInfo"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "shippingInfo") {
|
||||
return <ShippingInfo key={"shippingInfo"} module={module} />;
|
||||
}
|
||||
if (module.type === "weightInfo") {
|
||||
return <WeightInfo key={"weightInfo"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "weightInfo") {
|
||||
return <WeightInfo key={"weightInfo"} module={module} />;
|
||||
}
|
||||
if (module.type === "packingSpec") {
|
||||
return <PackingSpec key={"packingSpec"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "packingSpec") {
|
||||
return <PackingSpec key={"packingSpec"} module={module} />;
|
||||
}
|
||||
if (module.type === "vehicleInfo") {
|
||||
return <VehicleInfo key={"vehicleInfo"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "vehicleInfo") {
|
||||
return <VehicleInfo key={"vehicleInfo"} module={module} />;
|
||||
}
|
||||
if (module.type === "otherFees") {
|
||||
return <OtherFees key={"otherFees"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "otherFees") {
|
||||
return <OtherFees key={"otherFees"} module={module} />;
|
||||
}
|
||||
if (module.type === "totalAmount") {
|
||||
return <TotalAmount key={"totalAmount"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "totalAmount") {
|
||||
return <TotalAmount key={"totalAmount"} module={module} />;
|
||||
}
|
||||
|
||||
if (module.type === "otherInfo") {
|
||||
return <OtherInfo key={"otherInfo"} module={module} />;
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
if (module.type === "otherInfo") {
|
||||
return <OtherInfo key={"otherInfo"} module={module} />;
|
||||
}
|
||||
})}
|
||||
</ImagePreview>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import { Icon } from "@/components";
|
||||
import { business, poster } from "@/services";
|
||||
import {
|
||||
exportProfitTableExcel,
|
||||
OrderCalculator,
|
||||
ProfitTableRow,
|
||||
ProfitTableTemplate,
|
||||
} from "@/utils";
|
||||
@ -256,35 +255,6 @@ export default function Step3Success(props: Step3SuccessProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// 计算单车的利润数据
|
||||
const calculateOrderProfit = (order: BusinessAPI.OrderVO): ProfitTableRow => {
|
||||
// 车次
|
||||
const vehicleNo = `第${order.orderVehicle.vehicleNo || ""}车`;
|
||||
|
||||
// 发货日期 - 从 orderShipList 获取
|
||||
const shippingDate = dayjs(order.orderVehicle.deliveryTime).format(
|
||||
"YYYY/MM/DD",
|
||||
);
|
||||
|
||||
const orderCalculator = new OrderCalculator(order);
|
||||
// 计算产地成本
|
||||
const originCost = orderCalculator
|
||||
.getCostCalculator()
|
||||
.calculateMelonPurchaseCost();
|
||||
// 报价金额
|
||||
const quoteAmount = orderCalculator.getSalesAmount();
|
||||
// 计算利润
|
||||
const profit = orderCalculator.getPersonalProfit();
|
||||
|
||||
return {
|
||||
vehicleNo,
|
||||
shippingDate,
|
||||
originCost,
|
||||
quoteAmount,
|
||||
profit,
|
||||
};
|
||||
};
|
||||
|
||||
// 生成利润表
|
||||
const generateProfitTable = async () => {
|
||||
try {
|
||||
|
||||
@ -71,7 +71,6 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
const [formErrors, setFormErrors] = useState<{
|
||||
warehouse?: boolean;
|
||||
estimatedArrivalDate?: boolean;
|
||||
remark?: boolean;
|
||||
}>({});
|
||||
|
||||
// 暴露方法给父组件
|
||||
@ -233,7 +232,7 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
备注
|
||||
</View>
|
||||
<View
|
||||
className={`flex w-full items-center rounded-md ${formErrors.remark ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
|
||||
className={`flex w-full items-center rounded-md border-4 border-gray-300`}
|
||||
>
|
||||
<TextArea
|
||||
className={"flex-1"}
|
||||
@ -243,14 +242,6 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
showCount
|
||||
value={orderShipVO?.remark || ""}
|
||||
onChange={(value) => {
|
||||
// 清除错误状态
|
||||
if (formErrors.remark && value) {
|
||||
setFormErrors((prev) => ({
|
||||
...prev,
|
||||
remark: false,
|
||||
}));
|
||||
}
|
||||
|
||||
setOrderShipVO((prev: any) => {
|
||||
return {
|
||||
...prev!,
|
||||
|
||||
@ -51,7 +51,7 @@ export type { MarketPriceSectionRef } from "./section/MarketPriceSection";
|
||||
export { default as RebateCalcSection } from "./section/RebateCalcSection";
|
||||
export { default as TaxSubsidySection } from "./section/TaxSubsidySection";
|
||||
export { default as TaxProvisionSection } from "./section/TaxProvisionSection";
|
||||
export { default as CostDifferenceSection } from "./section/CostDifferenceSection";
|
||||
export { CostDifferenceSection } from "./section/CostDifferenceSection";
|
||||
export { default as MaterialCostSection } from "./section/MaterialCostSection";
|
||||
export { default as ProductionAdvanceSection } from "./section/ProductionAdvanceSection";
|
||||
export { default as WorkerAdvanceSection } from "./section/WorkerAdvanceSection";
|
||||
|
||||
@ -770,6 +770,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
|
||||
console.log("识别结果为:", res.result);
|
||||
setSupplierVO({
|
||||
...supplierVO,
|
||||
bankName: res.result.bankName,
|
||||
bankCard: res.result.number,
|
||||
});
|
||||
|
||||
|
||||
@ -145,11 +145,21 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
className="bg-primary/10 flex flex-col gap-2.5 rounded-lg p-2.5"
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<View className="relative mr-3 h-16 w-16 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
className="h-full w-full object-cover"
|
||||
src={uploadFileItem.filePath!}
|
||||
/>
|
||||
<View className="relative mr-3 h-16 w-16 flex-shrink-0 overflow-hidden rounded-lg">
|
||||
{uploadFileItem.fileType !== "pdf" ? (
|
||||
<Image
|
||||
className="h-full w-full object-cover"
|
||||
src={uploadFileItem.filePath!}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
className={
|
||||
"bg-primary/10 flex h-16 w-16 items-center justify-center"
|
||||
}
|
||||
name="file-pdf"
|
||||
size={32}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<View className="font-medium" id="invoice-filename">
|
||||
@ -222,6 +232,14 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
setLoading(true);
|
||||
const file = res.tempFiles[0];
|
||||
uploadFile(file.path).then(({ url }) => {
|
||||
if (!url) {
|
||||
Toast.show("toast", {
|
||||
title: "上传失败",
|
||||
icon: "fail",
|
||||
content: "上传失败",
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
setSupplierVO({
|
||||
...supplierVO!,
|
||||
invoiceImg: [
|
||||
@ -263,6 +281,14 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
setLoading(true);
|
||||
const file = res.tempFiles[0];
|
||||
uploadFile(file.path).then(({ url }) => {
|
||||
if (!url) {
|
||||
Toast.show("toast", {
|
||||
title: "上传失败",
|
||||
icon: "fail",
|
||||
content: "上传失败",
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
setSupplierVO({
|
||||
...supplierVO!,
|
||||
invoiceImg: [
|
||||
@ -319,7 +345,7 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
className="bg-primary/10 flex flex-col gap-2.5 rounded-lg p-2.5"
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<View className="relative mr-3 h-16 w-16 overflow-hidden rounded-lg">
|
||||
<View className="relative mr-3 h-16 w-16 flex-shrink-0 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
className="h-full w-full object-cover"
|
||||
src={uploadFileItem.filePath!}
|
||||
@ -355,6 +381,11 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
(newContractImg && newContractImg.length > 0) ||
|
||||
false,
|
||||
});
|
||||
Toast.show("toast", {
|
||||
title: "删除成功",
|
||||
icon: "success",
|
||||
content: "合同已删除",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<View>删除</View>
|
||||
@ -376,12 +407,22 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
success: (res) => {
|
||||
setLoading(true);
|
||||
const uploadPromises = res.tempFiles.map((file) =>
|
||||
uploadFile(file.path).then(({ url }) => ({
|
||||
fileName: url.split("/").pop(),
|
||||
filePath: url,
|
||||
fileSize: file.size,
|
||||
fileType: url.split(".").pop(),
|
||||
})),
|
||||
uploadFile(file.path).then(({ url }) => {
|
||||
if (!url) {
|
||||
Toast.show("toast", {
|
||||
title: "上传失败",
|
||||
icon: "fail",
|
||||
content: "上传失败",
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
return {
|
||||
fileName: url.split("/").pop(),
|
||||
filePath: url,
|
||||
fileSize: file.size,
|
||||
fileType: url.split(".").pop(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Promise.all(uploadPromises)
|
||||
@ -438,12 +479,22 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
||||
success: (res) => {
|
||||
setLoading(true);
|
||||
const uploadPromises = res.tempFiles.map((file) =>
|
||||
uploadFile(file.path).then(({ url }) => ({
|
||||
fileName: url.split("/").pop(),
|
||||
filePath: url,
|
||||
fileSize: file.size,
|
||||
fileType: url.split(".").pop(),
|
||||
})),
|
||||
uploadFile(file.path).then(({ url }) => {
|
||||
if (!url) {
|
||||
Toast.show("toast", {
|
||||
title: "上传失败",
|
||||
icon: "fail",
|
||||
content: "上传失败",
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
return {
|
||||
fileName: url.split("/").pop(),
|
||||
filePath: url,
|
||||
fileSize: file.size,
|
||||
fileType: url.split(".").pop(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Promise.all(uploadPromises)
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { Text, View } from "@tarojs/components";
|
||||
import { OrderCalculator } from "@/utils";
|
||||
import { PriceEditor } from "@/components";
|
||||
import { OrderCalculator, ProfitTableRow } from "@/utils";
|
||||
import { Icon, ImagePreview, PriceEditor } from "@/components";
|
||||
import { Button, Popup } from "@nutui/nutui-react-taro";
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { business } from "@/services";
|
||||
import { ProfitCalculationService } from "@/utils/classes/calculators";
|
||||
|
||||
export default function CostDifferenceSection(props: {
|
||||
export function CostDifferenceSection(props: {
|
||||
orderVO: BusinessAPI.OrderVO;
|
||||
onChange?: (orderVO: BusinessAPI.OrderVO) => void;
|
||||
readOnly?: boolean;
|
||||
@ -11,6 +16,74 @@ export default function CostDifferenceSection(props: {
|
||||
const { orderVO, onChange, readOnly, calculator } = props;
|
||||
const orderDealer = orderVO.orderDealer;
|
||||
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
|
||||
// 获取当前月份
|
||||
const currentMonth = dayjs(orderVO.orderVehicle.deliveryTime).format(
|
||||
"YYYY-MM-01",
|
||||
);
|
||||
const [profitData, setProfitData] = useState<ProfitTableRow[]>([]);
|
||||
|
||||
const init = async (currentMonth: string) => {
|
||||
// 查询本月的所有订单数据
|
||||
let {
|
||||
data: { data: orderVOList = [] },
|
||||
} = await business.order.listOrder({
|
||||
orderListQry: {
|
||||
dealerId: orderVO.orderDealer.dealerId,
|
||||
month: currentMonth,
|
||||
state: "COMPLETED", // 只查询已完成的订单
|
||||
},
|
||||
});
|
||||
|
||||
orderVOList.push(orderVO);
|
||||
// 获取当前车次
|
||||
const currentVehicleNo = orderVO.orderVehicle.vehicleNo;
|
||||
|
||||
// 筛选截止到当前车次之前的数据
|
||||
const profitData: ProfitTableRow[] = [];
|
||||
let includeNext = true;
|
||||
|
||||
// orderVOList 根据车次正序
|
||||
orderVOList.sort((a, b) => {
|
||||
return (
|
||||
Number(a.orderVehicle.vehicleNo) - Number(b.orderVehicle?.vehicleNo)
|
||||
);
|
||||
});
|
||||
|
||||
for (const order of orderVOList) {
|
||||
// 如果还没有到达当前车次,继续
|
||||
if (includeNext) {
|
||||
const profitRow = ProfitCalculationService.calculateOrderProfit(order);
|
||||
profitData.push(profitRow);
|
||||
|
||||
// 如果是当前车次,停止添加
|
||||
if (order.orderVehicle.vehicleNo === currentVehicleNo) {
|
||||
includeNext = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setProfitData(profitData);
|
||||
};
|
||||
|
||||
// 预览发货单操作
|
||||
const handlePreview = () => {
|
||||
init(currentMonth);
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
// 计算合计
|
||||
const totalOriginCost = profitData.reduce(
|
||||
(sum, row) => sum + row.originCost,
|
||||
0,
|
||||
);
|
||||
const totalQuoteAmount = profitData.reduce(
|
||||
(sum, row) => sum + row.quoteAmount,
|
||||
0,
|
||||
);
|
||||
const totalProfit = profitData.reduce((sum, row) => sum + row.profit, 0);
|
||||
|
||||
return (
|
||||
<View className={"flex flex-col gap-2.5"}>
|
||||
{/* 卡片形式展示分成信息 */}
|
||||
@ -56,6 +129,105 @@ export default function CostDifferenceSection(props: {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 预览利润表 */}
|
||||
<View className={"flex-1"}>
|
||||
<Button
|
||||
icon={<Icon name="eye" size={18} />}
|
||||
size={"large"}
|
||||
type="primary"
|
||||
fill="outline"
|
||||
block
|
||||
onClick={handlePreview}
|
||||
>
|
||||
预览利润表
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 预览发货单 */}
|
||||
<Popup
|
||||
duration={150}
|
||||
style={{
|
||||
minHeight: "auto",
|
||||
}}
|
||||
className={"!bg-[#D1D5DB]"}
|
||||
visible={previewVisible}
|
||||
position="bottom"
|
||||
onClose={() => {
|
||||
setPreviewVisible(false);
|
||||
}}
|
||||
closeable
|
||||
destroyOnClose
|
||||
title={"预览利润单"}
|
||||
>
|
||||
<View className={"overflow-hidden p-2.5"}>
|
||||
<ImagePreview>
|
||||
<View className="table-border">
|
||||
<View className="table-title flex items-end justify-center text-base">
|
||||
诚信志远利润明细
|
||||
</View>
|
||||
<View className={"grid w-full grid-cols-5 gap-0 text-base"}>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
车次
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
发货日期
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
产地成本
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
报价金额
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
利润
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{profitData.map((row, index) => (
|
||||
<View
|
||||
className={"grid w-full grid-cols-5 gap-0 text-base"}
|
||||
key={index}
|
||||
>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{row.vehicleNo}
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{row.shippingDate}
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{row.originCost}
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{row.quoteAmount}
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{row.profit.toFixed(2)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<View className={"grid w-full grid-cols-5 gap-0 text-base"}>
|
||||
<View
|
||||
className={
|
||||
"col-span-2 flex items-end justify-center bg-yellow-500"
|
||||
}
|
||||
>
|
||||
{dayjs(currentMonth).format("MMMM")}合计
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{totalOriginCost.toFixed(2)}
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{totalQuoteAmount.toFixed(2)}
|
||||
</View>
|
||||
<View className={"col-span-1 flex items-end justify-center"}>
|
||||
{totalProfit.toFixed(2)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ImagePreview>
|
||||
</View>
|
||||
</Popup>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
// App 相关常量
|
||||
export const APP_VERSION = "v0.0.73";
|
||||
export const APP_VERSION = "v0.0.75";
|
||||
|
||||
@ -285,9 +285,9 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
}
|
||||
// 检查预计到仓时间字段是否开启且已填写
|
||||
else if (column.dataIndex === "requiredEstimatedArrivalTime") {
|
||||
formData.estimatedArrivalDate = dayjs(
|
||||
orderShip?.estimatedArrivalDate,
|
||||
).format("YYYY-MM-DD");
|
||||
formData.estimatedArrivalDate = orderShip?.estimatedArrivalDate
|
||||
? dayjs(orderShip?.estimatedArrivalDate).format("YYYY-MM-DD")
|
||||
: undefined;
|
||||
}
|
||||
// 检查备注字段是否开启且已填写
|
||||
else if (column.dataIndex === "requiredRemarks") {
|
||||
|
||||
@ -38,7 +38,7 @@ export default function Page() {
|
||||
|
||||
const [data, setData] = useState({
|
||||
skipphotoStatus: "0",
|
||||
flash: true,
|
||||
flash: false,
|
||||
bottom: 125,
|
||||
windowWidth: 300,
|
||||
touchPadding: 36,
|
||||
|
||||
@ -7,3 +7,4 @@
|
||||
|
||||
export { OrderCalculator } from "./OrderCalculator";
|
||||
export { WeightCalculationService } from "./services/WeightCalculationService";
|
||||
export { ProfitCalculationService } from "./services/ProfitCalculationService";
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { OrderCalculator, ProfitTableRow } from "@/utils";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export class ProfitCalculationService {
|
||||
// 计算单车的利润数据
|
||||
|
||||
static calculateOrderProfit(order: BusinessAPI.OrderVO): ProfitTableRow {
|
||||
// 车次
|
||||
const vehicleNo = `第${order.orderVehicle.vehicleNo || ""}车`;
|
||||
|
||||
// 发货日期 - 从 orderShipList 获取
|
||||
const shippingDate = dayjs(order.orderVehicle.deliveryTime).format(
|
||||
"YYYY/MM/DD",
|
||||
);
|
||||
|
||||
const orderCalculator = new OrderCalculator(order);
|
||||
// 计算产地成本
|
||||
const originCost = orderCalculator
|
||||
.getCostCalculator()
|
||||
.calculateMelonPurchaseCost();
|
||||
// 报价金额
|
||||
const quoteAmount = orderCalculator.getSalesAmount();
|
||||
// 计算利润
|
||||
const profit = orderCalculator.getPersonalProfit();
|
||||
|
||||
return {
|
||||
vehicleNo,
|
||||
shippingDate,
|
||||
originCost,
|
||||
quoteAmount,
|
||||
profit,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -121,5 +121,6 @@ export const convertOrderToOrderShip = (
|
||||
state: "WAIT_PAYMENT",
|
||||
receivableAmount: calculator.getMarketPrice(),
|
||||
orderShipItemList,
|
||||
remark: oldOrderShip?.remark || "",
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user