feat(app-client): 优化采购订单审核与供应商发票上传功能

- 优化 PageList 组件的选中行样式与全选逻辑
- 重构采购订单相关组件,移除冗余的 dealerVO 属性,统一使用 orderDealer
- 完善成本差异、税金计提、税金补贴等模块的默认值计算逻辑
- 新增供应商发票与合同照片上传弹窗功能
- 改进供应商页面的车次选择与数据统计展示
- 升级 app 版本号至 v0.0.19
- 优化头像图片样式与组件代码结构
- 修复 dealerVO 数据获取逻辑,替换为 dealerRebateCustomer 数据源
- 增加页面加载状态控制与条件渲染逻辑
- 完善 uploader 组件的文件变更处理与提交逻辑
This commit is contained in:
shenyifei 2025-11-18 22:55:57 +08:00
parent 761bc7c8ed
commit 5bdd8afdd6
10 changed files with 959 additions and 1191 deletions

View File

@ -248,7 +248,7 @@ export default <T extends {}, Q extends Query = Query>(
list={data}
itemRender={(item, index) =>
(toolbar?.selectRow && (
<View className={"flex items-center justify-center"}>
<View className={"flex items-center justify-center bg-white"}>
<Checkbox
className={"pl-2"}
checked={selectRows.indexOf(item[rowId]) != -1}
@ -306,7 +306,9 @@ export default <T extends {}, Q extends Query = Query>(
(item, index) =>
(toolbar?.selectRow && (
<View
className={"flex flex-1 items-center gap-2.5"}
className={
"mb-2.5 flex flex-1 items-start gap-2.5 rounded-md bg-white p-2.5 shadow-md"
}
key={index}
>
<Checkbox
@ -364,8 +366,8 @@ export default <T extends {}, Q extends Query = Query>(
setSelectAll("1");
data?.forEach((item) => {
if (
selectRows.some(
(item1) => item1[rowId] !== item[rowId],
!selectRows.some(
(item1) => item1[rowId] === item[rowId],
)
) {
setSelectRows((prev) => {
@ -387,6 +389,7 @@ export default <T extends {}, Q extends Query = Query>(
<Button
type={"primary"}
size={"large"}
disabled={!selectRows.length}
block={false}
onClick={async () => toolbar?.selectRow?.onClick(selectRows)}
>

View File

@ -5,34 +5,31 @@ import { validatePrice } from "@/utils/format";
import { PurchaseOrderCalculator } from "@/utils/PurchaseOrderCalculator";
export default function CostDifferenceSection(props: {
dealerVO: BusinessAPI.DealerVO;
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
onChange?: (purchaseOrderVO: BusinessAPI.PurchaseOrderVO) => void;
readOnly?: boolean;
}) {
const { purchaseOrderVO, onChange, readOnly } = props;
const orderDealer = purchaseOrderVO.orderDealer;
const calculator = new PurchaseOrderCalculator(purchaseOrderVO);
const [visible, setVisible] = useState(false);
// 主状态,用于页面显示
const [costDifference, setCostDifference] = useState<number>(
purchaseOrderVO.orderDealer?.costDifference || 0,
orderDealer?.costDifference || 0,
);
// 弹窗内的临时状态
const [tempCostDifference, setTempCostDifference] = useState<number>(
purchaseOrderVO.orderDealer?.costDifference || 0,
orderDealer?.costDifference || 0,
);
const profitSharing =
calculator.getDefaultNetProfit() ||
purchaseOrderVO.orderDealer?.profitSharing ||
0;
calculator.getDefaultNetProfit() || orderDealer?.profitSharing || 0;
// 当dealerVO变化时自动计算分成
useEffect(() => {
if (!purchaseOrderVO.orderDealer?.costDifference) {
if (!orderDealer?.costDifference) {
const defaultCostDifference = calculator.getCostDifference();
setCostDifference(defaultCostDifference);
setTempCostDifference(defaultCostDifference);
@ -40,7 +37,7 @@ export default function CostDifferenceSection(props: {
onChange?.({
...purchaseOrderVO,
orderDealer: {
...purchaseOrderVO.orderDealer,
...orderDealer,
costDifference: defaultCostDifference,
profitSharing: profitSharing,
},
@ -56,7 +53,7 @@ export default function CostDifferenceSection(props: {
onChange?.({
...purchaseOrderVO,
orderDealer: {
...purchaseOrderVO.orderDealer,
...orderDealer,
costDifference: tempCostDifference,
profitSharing: profitSharing,
},

View File

@ -33,15 +33,7 @@ export default function (props: {
if (readOnly) return;
const newOrderDealer: BusinessAPI.OrderDealer = {
dealerId: dealerVO.dealerId,
shortName: dealerVO?.shortName!,
dealerType: dealerVO.dealerType!,
enableShare: dealerVO.enableShare,
shareRatio: dealerVO.shareRatio,
freightCostFlag: dealerVO.freightCostFlag,
strawMatCostFlag: dealerVO.strawMatCostFlag,
includePackingFlag: dealerVO.includePackingFlag,
documentTypes: dealerVO.documentTypes,
...dealerVO,
};
setOrderDealer(newOrderDealer);

View File

@ -5,30 +5,30 @@ import { validatePrice } from "@/utils/format";
import { PurchaseOrderCalculator } from "@/utils/PurchaseOrderCalculator";
export default function TaxProvisionSection(props: {
dealerVO: BusinessAPI.DealerVO;
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
onChange?: (purchaseOrderVO: BusinessAPI.PurchaseOrderVO) => void;
readOnly?: boolean;
}) {
const { purchaseOrderVO, onChange, readOnly, dealerVO } = props;
const { purchaseOrderVO, onChange, readOnly } = props;
const orderDealer = purchaseOrderVO.orderDealer;
const calculator = new PurchaseOrderCalculator(purchaseOrderVO);
const [visible, setVisible] = useState(false);
const [taxProvision, setTaxProvision] = useState<number>(
purchaseOrderVO.orderDealer?.taxProvision || 0,
orderDealer?.taxProvision || 0,
);
// 当dealerVO变化时自动计算计提税金
useEffect(() => {
if (!purchaseOrderVO.orderDealer?.taxProvision) {
if (!orderDealer?.taxProvision) {
const defaultValue = calculator.getDefaultTaxProvision();
setTaxProvision(defaultValue);
// 更新父组件的状态
onChange?.({
...purchaseOrderVO,
orderDealer: {
...purchaseOrderVO.orderDealer,
...orderDealer,
taxProvision: defaultValue,
},
});
@ -48,7 +48,7 @@ export default function TaxProvisionSection(props: {
onChange?.({
...purchaseOrderVO,
orderDealer: {
...purchaseOrderVO.orderDealer,
...orderDealer,
taxProvision: taxProvision,
},
});
@ -97,8 +97,8 @@ export default function TaxProvisionSection(props: {
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-500"></Text>
<Text className="text-sm font-medium">
{dealerVO.accrualTaxRatio
? dealerVO.accrualTaxRatio + "%"
{orderDealer.accrualTaxRatio
? orderDealer.accrualTaxRatio + "%"
: "未设置"}
</Text>
</View>
@ -151,7 +151,7 @@ export default function TaxProvisionSection(props: {
</Button>
<View className="ml-2 self-center text-xs text-gray-500">
: ( - ) ×{" "}
{dealerVO.accrualTaxRatio || "未设置"}%
{orderDealer.accrualTaxRatio || "未设置"}%
</View>
</View>
</View>

View File

@ -5,30 +5,29 @@ import { validatePrice } from "@/utils/format";
import { PurchaseOrderCalculator } from "@/utils/PurchaseOrderCalculator";
export default function TaxSubsidySection(props: {
dealerVO: BusinessAPI.DealerVO;
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
onChange?: (purchaseOrderVO: BusinessAPI.PurchaseOrderVO) => void;
readOnly?: boolean;
}) {
const { purchaseOrderVO, onChange, readOnly, dealerVO } = props;
const { purchaseOrderVO, onChange, readOnly } = props;
const orderDealer = purchaseOrderVO.orderDealer;
const calculator = new PurchaseOrderCalculator(purchaseOrderVO);
const [visible, setVisible] = useState(false);
const [taxSubsidy, setTaxSubsidy] = useState<number>(
purchaseOrderVO.orderDealer?.taxSubsidy || 0,
orderDealer?.taxSubsidy || 0,
);
// 当dealerVO变化时自动计算税费补贴
useEffect(() => {
if (!purchaseOrderVO.orderDealer?.taxSubsidy) {
if (!orderDealer?.taxSubsidy) {
const defaultValue = calculator.getDefaultTaxSubsidy();
setTaxSubsidy(defaultValue);
// 更新父组件的状态
onChange?.({
...purchaseOrderVO,
orderDealer: {
...purchaseOrderVO.orderDealer,
...orderDealer,
taxSubsidy: defaultValue,
},
});
@ -46,7 +45,7 @@ export default function TaxSubsidySection(props: {
onChange?.({
...purchaseOrderVO,
orderDealer: {
...purchaseOrderVO.orderDealer,
...orderDealer,
taxSubsidy,
},
});
@ -95,8 +94,8 @@ export default function TaxSubsidySection(props: {
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-500"></Text>
<Text className="text-sm font-medium">
{dealerVO.companyRebateRatio
? dealerVO.companyRebateRatio + "%"
{orderDealer.companyRebateRatio
? orderDealer.companyRebateRatio + "%"
: "未设置"}
</Text>
</View>
@ -148,7 +147,7 @@ export default function TaxSubsidySection(props: {
</Button>
<View className="ml-2 self-center text-xs text-gray-500">
默认值: 市场报价 × {dealerVO.companyRebateRatio || "未设置"}%
默认值: 市场报价 × {orderDealer.companyRebateRatio || "未设置"}%
</View>
</View>
</View>

View File

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

View File

@ -170,7 +170,7 @@ export default hocAuth(function Page(props: CommonComponent) {
<Image
src={employeeVO?.avatar}
mode={"aspectFill"}
className={"rounded-full"}
className={"h-full w-full overflow-hidden rounded-full"}
/>
</View>
) : (

View File

@ -5,7 +5,16 @@ import { business } from "@/services";
import { useEffect, useState } from "react";
import { View } from "@tarojs/components";
import purchaseOrder from "@/constant/purchaseOrder";
import { ActionSheet, Button, Dialog, Input, Popup, SafeArea, TextArea, Toast } from "@nutui/nutui-react-taro";
import {
ActionSheet,
Button,
Dialog,
Input,
Popup,
SafeArea,
TextArea,
Toast,
} from "@nutui/nutui-react-taro";
import {
BasicInfoSection,
CompanyInfoSection,
@ -22,7 +31,7 @@ import {
State,
TaxProvisionSection,
TaxSubsidySection,
WorkerAdvanceSection
WorkerAdvanceSection,
} from "@/components";
import buildUrl from "@/utils/buildUrl";
import { PurchaseOrderCalculator } from "@/utils/PurchaseOrderCalculator";
@ -116,7 +125,7 @@ const sections = {
};
export default hocAuth(function Page(props: CommonComponent) {
const { router, isInitialized, setIsInitialized, role } = props;
const { router, isInitialized, setIsInitialized, role, setLoading } = props;
const orderId = router.params
.orderId as BusinessAPI.PurchaseOrderVO["orderId"];
@ -216,17 +225,20 @@ export default hocAuth(function Page(props: CommonComponent) {
// 控制更多操作的ActionSheet显示状态
const [moreActionVisible, setMoreActionVisible] = useState(false);
const [dealerVO, setDealerVO] = useState<BusinessAPI.DealerVO>();
const [dealerRebateCustomerVOList, setDealerRebateCustomerVOList] =
useState<BusinessAPI.DealerRebateCustomerVO[]>();
const initDealer = async (dealerId: BusinessAPI.DealerVO["dealerId"]) => {
const { data } = await business.dealer.showDealer({
dealerShowQry: {
const {
data: { data: dealerRebateCustomerVOList },
} = await business.dealerRebateCustomer.listDealerRebateCustomer({
dealerRebateCustomerListQry: {
dealerId: dealerId,
status: true,
},
});
setDealerVO(data.data);
setDealerRebateCustomerVOList(dealerRebateCustomerVOList);
};
// 暂存操作
@ -357,8 +369,10 @@ export default hocAuth(function Page(props: CommonComponent) {
useEffect(() => {
if (orderId && !isInitialized) {
setLoading(true);
init(orderId).then(() => {
setIsInitialized(true);
setLoading(false);
});
}
}, []);
@ -369,7 +383,7 @@ export default hocAuth(function Page(props: CommonComponent) {
}
});
if (!purchaseOrderVO) {
if (!purchaseOrderVO || !dealerRebateCustomerVOList) {
return;
}
@ -436,16 +450,32 @@ export default hocAuth(function Page(props: CommonComponent) {
{/* 循环渲染各部分内容 */}
{Object.keys(sections).map((sectionKey) => {
const section = sections[sectionKey];
const orderDealer = purchaseOrderVO.orderDealer;
if (!dealerVO?.enableCompanyRebate && sectionKey === "taxSubsidy") {
if (
!orderDealer?.enableCompanyRebate &&
sectionKey === "taxSubsidy"
) {
return null;
}
if (!dealerVO?.enableAccrualTax && sectionKey === "taxProvision") {
if (
!orderDealer?.enableAccrualTax &&
sectionKey === "taxProvision"
) {
return null;
}
if (!dealerVO?.enableShare && sectionKey === "costDifference") {
if (!orderDealer?.enableShare && sectionKey === "costDifference") {
return null;
}
// 如果没有返点人这个模块,则不渲染
if (
(!dealerRebateCustomerVOList ||
dealerRebateCustomerVOList.length === 0) &&
sectionKey === "rebateCalc"
) {
return null;
}
@ -456,7 +486,6 @@ export default hocAuth(function Page(props: CommonComponent) {
className={`overflow-x-auto rounded-md rounded-b-lg bg-white p-2.5 shadow-sm`}
>
<section.component
dealerVO={dealerVO}
purchaseOrderVO={purchaseOrderVO}
onChange={setPurchaseOrderVO}
costItemVOList={costItemVOList}

View File

@ -1,10 +1,4 @@
import {
ActionType,
Icon,
PageList,
SupplierPicker,
ToolBar,
} from "@/components";
import { ActionType, Icon, PageList, SupplierPicker, ToolBar } from "@/components";
import { useShareAppMessage } from "@tarojs/taro";
import { useRef, useState } from "react";
import { business } from "@/services";
@ -12,17 +6,69 @@ import hocAuth from "@/hocs/auth";
import { CommonComponent } from "@/types/typings";
import { View } from "@tarojs/components";
import dayjs from "dayjs";
import { Button, Popup, SafeArea, Toast, Uploader, UploaderFileItem } from "@nutui/nutui-react-taro";
import { uploadFile } from "@/utils/uploader";
export default hocAuth(function Page(props: CommonComponent) {
const { shareOptions } = props;
const [supplierVO, setSupplierVO] = useState<BusinessAPI.SupplierVO>();
const [popupVisible, setPopupVisible] = useState(false);
const [selectedOrders, setSelectedOrders] = useState<
BusinessAPI.OrderSupplierVO[]
>([]);
const [contractFiles, setContractFiles] = useState<any[]>([]);
const [invoiceFiles, setInvoiceFiles] = useState<any[]>([]);
// 发票照片
const [invoiceImgList, setInvoiceImgList] = useState<UploaderFileItem[]>([]);
// 合同照片
const [contractImgList, setContractImgList] = useState<UploaderFileItem[]>(
[],
);
// 发票照片变更处理函数
const handleInvoiceImgChange = (files: UploaderFileItem[]) => {
setInvoiceImgList(files);
// 如果有文件且上传成功保存URL到supplierVO
if (files.length > 0 && files[0].url) {
setSupplierVO((prev) => ({
...prev!,
invoiceImg: [files[0].url!],
invoiceUpload: true,
}));
} else {
// 如果没有文件清空URL
setSupplierVO((prev) => ({
...prev!,
invoiceImg: undefined,
invoiceUpload: false,
}));
}
};
// 合同照片变更处理函数
const handleContractImgChange = (files: UploaderFileItem[]) => {
setContractImgList(files);
// 保存所有文件URL到supplierVO
const urls = files.map((file) => file.url).filter((url) => url) as string[];
setSupplierVO((prev) => ({
...prev!,
contractImg: urls,
contractUpload: urls.length > 0,
}));
};
const actionRef = useRef<ActionType>();
const toolbar: ToolBar = {
selectRow: {
onClick: async (orderSupplierVOList: BusinessAPI.OrderSupplierVO[]) => {
console.log("orderSupplierVOList", orderSupplierVOList);
// 点击弹出popup
setSelectedOrders(orderSupplierVOList);
setPopupVisible(true);
},
},
search: {
@ -43,6 +89,116 @@ export default hocAuth(function Page(props: CommonComponent) {
},
render: () => (
<>
{/* Popup 弹窗 */}
<Popup
visible={popupVisible}
position="bottom"
onClose={() => setPopupVisible(false)}
onOverlayClick={() => setPopupVisible(false)}
lockScroll
round
>
<View className="border-t border-green-100 bg-green-50 p-3">
{/* 统计信息 */}
<View className="mb-2 flex items-center justify-between">
<View className="text-sm font-medium text-gray-800">
{" "}
<View className="text-primary inline">
{selectedOrders.length}
</View>{" "}
</View>
<View className="text-sm font-medium text-gray-800">
<View className="text-primary inline">{totalWeight}</View>
</View>
<View className="text-sm font-medium text-gray-800">
{" "}
<View className="text-primary inline">
{totalAmount.toFixed(2)}
</View>
</View>
</View>
{/* 上传发票 */}
<View className="mb-2 flex items-center justify-between">
<View className="flex-1">
<Uploader
className={"w-full"}
value={invoiceImgList}
onChange={handleInvoiceImgChange}
sourceType={["album", "camera"]}
uploadIcon={<Icon name={"camera"} size={36} />}
uploadLabel={
<View className={"flex flex-col items-center"}>
<View className="text-sm"></View>
<View className="mt-1 text-xs text-gray-400">
</View>
</View>
}
maxCount={1}
//@ts-ignore
upload={uploadFile}
multiple
/>
</View>
</View>
{/* 上传合同 */}
<View className="mb-2 flex items-center justify-between">
<View className="flex-1">
<Uploader
className={"w-full"}
value={contractImgList}
onChange={handleContractImgChange}
sourceType={["album", "camera"]}
uploadIcon={<Icon name={"camera"} size={36} />}
uploadLabel={
<View className={"flex flex-col items-center"}>
<View className="text-sm"></View>
<View className="mt-1 text-xs text-gray-400">
</View>
</View>
}
maxCount={9}
//@ts-ignore
upload={uploadFile}
multiple
/>
</View>
</View>
<View className={"flex flex-1 flex-row gap-2.5"}>
<View className={"flex-1"}>
<Button
size={"xlarge"}
block
type="default"
onClick={() => setPopupVisible(false)}
>
</Button>
</View>
{/* 提交按钮 */}
<View className={"flex-1"}>
<Button
size={"xlarge"}
block
type="primary"
disabled={
contractFiles.length === 0 || invoiceFiles.length === 0
}
onClick={handleSubmit}
>
</Button>
</View>
</View>
<SafeArea position={"bottom"} />
</View>
</Popup>
<View className={"flex flex-row gap-2.5"}>
<SupplierPicker
onFinish={(supplierVO) => {
@ -87,17 +243,42 @@ export default hocAuth(function Page(props: CommonComponent) {
return {};
});
// 计算选中车次的总重量和总金额
const calculateTotals = () => {
return selectedOrders.reduce(
(totals, order) => {
totals.totalWeight += order.netWeight || 0;
totals.totalAmount += order.invoiceAmount || 0;
return totals;
},
{ totalWeight: 0, totalAmount: 0 },
);
};
const { totalWeight, totalAmount } = calculateTotals();
// 提交申请
const handleSubmit = async () => {
// 这里添加提交申请的逻辑
Toast.show("提交申请成功", { duration: 1500 });
setPopupVisible(false);
// 重置状态
setContractFiles([]);
setInvoiceFiles([]);
};
return (
<>
<PageList<BusinessAPI.OrderSupplierVO, BusinessAPI.OrderSupplierPageQry>
rowId={"orderSupplierId"}
itemHeight={182}
type={"infinite"}
actionRef={actionRef}
render={(orderSupplierVO: BusinessAPI.OrderSupplierVO, index) => (
<View className={"mb-2.5 flex-1"} key={index}>
<View className={"flex-1"} key={index}>
<View
className={
"relative flex flex-col divide-y-2 divide-neutral-100 rounded-lg bg-white p-2.5"
"relative flex flex-col divide-y-2 divide-neutral-100"
}
>
<View className="flex-1">
@ -161,5 +342,6 @@ export default hocAuth(function Page(props: CommonComponent) {
pageSize: 10,
}}
/>
</>
);
});

File diff suppressed because it is too large Load Diff