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 { TabBar } from "./TabBar";
|
||||||
export { default as PriceEditor } from "./PriceEditor";
|
export { default as PriceEditor } from "./PriceEditor";
|
||||||
export { default as VersionChecker } from "./VersionChecker";
|
export { default as VersionChecker } from "./VersionChecker";
|
||||||
|
export { default as ImagePreview } from "./ImagePreview";
|
||||||
export type { TabBarProps } from "./TabBar";
|
export type { TabBarProps } from "./TabBar";
|
||||||
|
|
||||||
export * from "./typing";
|
export * from "./typing";
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { ImagePreview } from "@/components";
|
||||||
import { Text, View } from "@tarojs/components";
|
|
||||||
import { Button } from "@nutui/nutui-react-taro";
|
|
||||||
import {
|
import {
|
||||||
DealerInfo,
|
DealerInfo,
|
||||||
OtherFees,
|
OtherFees,
|
||||||
@ -20,167 +18,45 @@ interface Step2PreviewProps {
|
|||||||
export default function Step2Preview(props: Step2PreviewProps) {
|
export default function Step2Preview(props: Step2PreviewProps) {
|
||||||
const { moduleList } = props;
|
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 (
|
return (
|
||||||
<View className="flex flex-1 flex-col items-center overflow-hidden">
|
<ImagePreview>
|
||||||
{/* 缩放控制按钮 */}
|
{moduleList.map((module) => {
|
||||||
<View className="mb-2 flex w-full justify-center gap-2 bg-white p-2">
|
if (module.type === "title") {
|
||||||
<Button size="small" onClick={zoomOut}>
|
return <TitleInfo key={"title"} module={module} />;
|
||||||
-
|
}
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* 预览区域 */}
|
if (module.type === "dealerInfo") {
|
||||||
<View
|
return <DealerInfo key={"dealerInfo"} module={module} />;
|
||||||
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") {
|
if (module.type === "shippingInfo") {
|
||||||
return <DealerInfo key={"dealerInfo"} module={module} />;
|
return <ShippingInfo key={"shippingInfo"} module={module} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === "shippingInfo") {
|
if (module.type === "weightInfo") {
|
||||||
return <ShippingInfo key={"shippingInfo"} module={module} />;
|
return <WeightInfo key={"weightInfo"} module={module} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === "weightInfo") {
|
if (module.type === "packingSpec") {
|
||||||
return <WeightInfo key={"weightInfo"} module={module} />;
|
return <PackingSpec key={"packingSpec"} module={module} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === "packingSpec") {
|
if (module.type === "vehicleInfo") {
|
||||||
return <PackingSpec key={"packingSpec"} module={module} />;
|
return <VehicleInfo key={"vehicleInfo"} module={module} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === "vehicleInfo") {
|
if (module.type === "otherFees") {
|
||||||
return <VehicleInfo key={"vehicleInfo"} module={module} />;
|
return <OtherFees key={"otherFees"} module={module} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === "otherFees") {
|
if (module.type === "totalAmount") {
|
||||||
return <OtherFees key={"otherFees"} module={module} />;
|
return <TotalAmount key={"totalAmount"} module={module} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.type === "totalAmount") {
|
if (module.type === "otherInfo") {
|
||||||
return <TotalAmount key={"totalAmount"} module={module} />;
|
return <OtherInfo key={"otherInfo"} module={module} />;
|
||||||
}
|
}
|
||||||
|
})}
|
||||||
if (module.type === "otherInfo") {
|
</ImagePreview>
|
||||||
return <OtherInfo key={"otherInfo"} module={module} />;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { Icon } from "@/components";
|
|||||||
import { business, poster } from "@/services";
|
import { business, poster } from "@/services";
|
||||||
import {
|
import {
|
||||||
exportProfitTableExcel,
|
exportProfitTableExcel,
|
||||||
OrderCalculator,
|
|
||||||
ProfitTableRow,
|
ProfitTableRow,
|
||||||
ProfitTableTemplate,
|
ProfitTableTemplate,
|
||||||
} from "@/utils";
|
} 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 () => {
|
const generateProfitTable = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -71,7 +71,6 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
|||||||
const [formErrors, setFormErrors] = useState<{
|
const [formErrors, setFormErrors] = useState<{
|
||||||
warehouse?: boolean;
|
warehouse?: boolean;
|
||||||
estimatedArrivalDate?: boolean;
|
estimatedArrivalDate?: boolean;
|
||||||
remark?: boolean;
|
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
@ -233,7 +232,7 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
|||||||
备注
|
备注
|
||||||
</View>
|
</View>
|
||||||
<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
|
<TextArea
|
||||||
className={"flex-1"}
|
className={"flex-1"}
|
||||||
@ -243,14 +242,6 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
|||||||
showCount
|
showCount
|
||||||
value={orderShipVO?.remark || ""}
|
value={orderShipVO?.remark || ""}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
// 清除错误状态
|
|
||||||
if (formErrors.remark && value) {
|
|
||||||
setFormErrors((prev) => ({
|
|
||||||
...prev,
|
|
||||||
remark: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
setOrderShipVO((prev: any) => {
|
setOrderShipVO((prev: any) => {
|
||||||
return {
|
return {
|
||||||
...prev!,
|
...prev!,
|
||||||
|
|||||||
@ -51,7 +51,7 @@ 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";
|
||||||
export { default as CostDifferenceSection } from "./section/CostDifferenceSection";
|
export { CostDifferenceSection } from "./section/CostDifferenceSection";
|
||||||
export { default as MaterialCostSection } from "./section/MaterialCostSection";
|
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";
|
||||||
|
|||||||
@ -770,6 +770,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
|
|||||||
console.log("识别结果为:", res.result);
|
console.log("识别结果为:", res.result);
|
||||||
setSupplierVO({
|
setSupplierVO({
|
||||||
...supplierVO,
|
...supplierVO,
|
||||||
|
bankName: res.result.bankName,
|
||||||
bankCard: res.result.number,
|
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"
|
className="bg-primary/10 flex flex-col gap-2.5 rounded-lg p-2.5"
|
||||||
>
|
>
|
||||||
<View className="flex items-center">
|
<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
|
{uploadFileItem.fileType !== "pdf" ? (
|
||||||
className="h-full w-full object-cover"
|
<Image
|
||||||
src={uploadFileItem.filePath!}
|
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>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<View className="font-medium" id="invoice-filename">
|
<View className="font-medium" id="invoice-filename">
|
||||||
@ -222,6 +232,14 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const file = res.tempFiles[0];
|
const file = res.tempFiles[0];
|
||||||
uploadFile(file.path).then(({ url }) => {
|
uploadFile(file.path).then(({ url }) => {
|
||||||
|
if (!url) {
|
||||||
|
Toast.show("toast", {
|
||||||
|
title: "上传失败",
|
||||||
|
icon: "fail",
|
||||||
|
content: "上传失败",
|
||||||
|
});
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
setSupplierVO({
|
setSupplierVO({
|
||||||
...supplierVO!,
|
...supplierVO!,
|
||||||
invoiceImg: [
|
invoiceImg: [
|
||||||
@ -263,6 +281,14 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const file = res.tempFiles[0];
|
const file = res.tempFiles[0];
|
||||||
uploadFile(file.path).then(({ url }) => {
|
uploadFile(file.path).then(({ url }) => {
|
||||||
|
if (!url) {
|
||||||
|
Toast.show("toast", {
|
||||||
|
title: "上传失败",
|
||||||
|
icon: "fail",
|
||||||
|
content: "上传失败",
|
||||||
|
});
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
setSupplierVO({
|
setSupplierVO({
|
||||||
...supplierVO!,
|
...supplierVO!,
|
||||||
invoiceImg: [
|
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"
|
className="bg-primary/10 flex flex-col gap-2.5 rounded-lg p-2.5"
|
||||||
>
|
>
|
||||||
<View className="flex items-center">
|
<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
|
<Image
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
src={uploadFileItem.filePath!}
|
src={uploadFileItem.filePath!}
|
||||||
@ -355,6 +381,11 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
|||||||
(newContractImg && newContractImg.length > 0) ||
|
(newContractImg && newContractImg.length > 0) ||
|
||||||
false,
|
false,
|
||||||
});
|
});
|
||||||
|
Toast.show("toast", {
|
||||||
|
title: "删除成功",
|
||||||
|
icon: "success",
|
||||||
|
content: "合同已删除",
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View>删除</View>
|
<View>删除</View>
|
||||||
@ -376,12 +407,22 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const uploadPromises = res.tempFiles.map((file) =>
|
const uploadPromises = res.tempFiles.map((file) =>
|
||||||
uploadFile(file.path).then(({ url }) => ({
|
uploadFile(file.path).then(({ url }) => {
|
||||||
fileName: url.split("/").pop(),
|
if (!url) {
|
||||||
filePath: url,
|
Toast.show("toast", {
|
||||||
fileSize: file.size,
|
title: "上传失败",
|
||||||
fileType: url.split(".").pop(),
|
icon: "fail",
|
||||||
})),
|
content: "上传失败",
|
||||||
|
});
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fileName: url.split("/").pop(),
|
||||||
|
filePath: url,
|
||||||
|
fileSize: file.size,
|
||||||
|
fileType: url.split(".").pop(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Promise.all(uploadPromises)
|
Promise.all(uploadPromises)
|
||||||
@ -438,12 +479,22 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const uploadPromises = res.tempFiles.map((file) =>
|
const uploadPromises = res.tempFiles.map((file) =>
|
||||||
uploadFile(file.path).then(({ url }) => ({
|
uploadFile(file.path).then(({ url }) => {
|
||||||
fileName: url.split("/").pop(),
|
if (!url) {
|
||||||
filePath: url,
|
Toast.show("toast", {
|
||||||
fileSize: file.size,
|
title: "上传失败",
|
||||||
fileType: url.split(".").pop(),
|
icon: "fail",
|
||||||
})),
|
content: "上传失败",
|
||||||
|
});
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fileName: url.split("/").pop(),
|
||||||
|
filePath: url,
|
||||||
|
fileSize: file.size,
|
||||||
|
fileType: url.split(".").pop(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Promise.all(uploadPromises)
|
Promise.all(uploadPromises)
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import { Text, View } from "@tarojs/components";
|
import { Text, View } from "@tarojs/components";
|
||||||
import { OrderCalculator } from "@/utils";
|
import { OrderCalculator, ProfitTableRow } from "@/utils";
|
||||||
import { PriceEditor } from "@/components";
|
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;
|
orderVO: BusinessAPI.OrderVO;
|
||||||
onChange?: (orderVO: BusinessAPI.OrderVO) => void;
|
onChange?: (orderVO: BusinessAPI.OrderVO) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@ -11,6 +16,74 @@ export default function CostDifferenceSection(props: {
|
|||||||
const { orderVO, onChange, readOnly, calculator } = props;
|
const { orderVO, onChange, readOnly, calculator } = props;
|
||||||
const orderDealer = orderVO.orderDealer;
|
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 (
|
return (
|
||||||
<View className={"flex flex-col gap-2.5"}>
|
<View className={"flex flex-col gap-2.5"}>
|
||||||
{/* 卡片形式展示分成信息 */}
|
{/* 卡片形式展示分成信息 */}
|
||||||
@ -56,6 +129,105 @@ export default function CostDifferenceSection(props: {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
// App 相关常量
|
// 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") {
|
else if (column.dataIndex === "requiredEstimatedArrivalTime") {
|
||||||
formData.estimatedArrivalDate = dayjs(
|
formData.estimatedArrivalDate = orderShip?.estimatedArrivalDate
|
||||||
orderShip?.estimatedArrivalDate,
|
? dayjs(orderShip?.estimatedArrivalDate).format("YYYY-MM-DD")
|
||||||
).format("YYYY-MM-DD");
|
: undefined;
|
||||||
}
|
}
|
||||||
// 检查备注字段是否开启且已填写
|
// 检查备注字段是否开启且已填写
|
||||||
else if (column.dataIndex === "requiredRemarks") {
|
else if (column.dataIndex === "requiredRemarks") {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const [data, setData] = useState({
|
const [data, setData] = useState({
|
||||||
skipphotoStatus: "0",
|
skipphotoStatus: "0",
|
||||||
flash: true,
|
flash: false,
|
||||||
bottom: 125,
|
bottom: 125,
|
||||||
windowWidth: 300,
|
windowWidth: 300,
|
||||||
touchPadding: 36,
|
touchPadding: 36,
|
||||||
|
|||||||
@ -7,3 +7,4 @@
|
|||||||
|
|
||||||
export { OrderCalculator } from "./OrderCalculator";
|
export { OrderCalculator } from "./OrderCalculator";
|
||||||
export { WeightCalculationService } from "./services/WeightCalculationService";
|
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",
|
state: "WAIT_PAYMENT",
|
||||||
receivableAmount: calculator.getMarketPrice(),
|
receivableAmount: calculator.getMarketPrice(),
|
||||||
orderShipItemList,
|
orderShipItemList,
|
||||||
|
remark: oldOrderShip?.remark || "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user