ERPTurbo_Client/packages/app-client/src/pages/supplier/create.tsx
shenyifei 31ece8807a feat(expenses): 新增费用与计提管理功能
- 添加费用类型选择器组件 CostPicker,支持搜索和选择费用类型
- 实现费用录入卡片 ExpenseCostCard,支持编辑和删除操作
- 创建费用录入弹窗 ExpenseCostCreate,包含费用类型、金额和备注字段
- 开发费用列表组件 ExpenseCostList,展示已录入费用并计算合计金额
- 实现计提记录卡片 ExpenseProvisionCard,支持编辑和删除计提信息
- 添加计提录入弹窗 ExpenseProvisionCreate,集成客户选择和金额输入
- 创建计提列表组件 ExpenseProvisionList,按客户分组展示计提记录
- 更新图标组件 Icon,新增 trash-can、pen、chevron-up 和 building 图标
- 导出所有费用相关组件,便于在其他模块中复用
2025-12-19 12:05:58 +08:00

656 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Image, View } from "@tarojs/components";
import { Button, Input, SafeArea, Toast } from "@nutui/nutui-react-taro";
import { Icon } from "@/components";
import hocAuth from "@/hocs/auth";
import { CommonComponent } from "@/types/typings";
import { useEffect, useState } from "react";
import { business } from "@/services";
import Taro from "@tarojs/taro";
import { buildUrl, generateShortId, uploadFile } from "@/utils";
export default hocAuth(function Page(props: CommonComponent) {
const { setLoading, router } = props;
const supplierId = router.params
.supplierId as BusinessAPI.SupplierVO["supplierId"];
const [supplierVO, setSupplierVO] = useState<BusinessAPI.SupplierCreateCmd>({
supplierId: generateShortId(),
name: "",
idCard: "",
phone: "",
bankCard: "",
status: true,
});
const [btnLoading, setBtnLoading] = useState(false);
// 错误状态
const [nameError, setNameError] = useState(false);
const [idCardError, setIdCardError] = useState(false);
const [bankCardError, setBankCardError] = useState(false);
const [phoneError, setPhoneError] = useState(false);
const [nameDuplicateError, setNameDuplicateError] = useState(false); // 姓名重复错误
// 初始化微信二维码图片列表
useEffect(() => {
if (supplierId) {
setLoading(true);
business.supplier
.showSupplier({
supplierShowQry: {
supplierId: supplierId,
},
})
.then(({ data: { data: supplierVO } }) => {
if (supplierVO) {
setSupplierVO({
...supplierVO,
});
}
})
.finally(() => {
setLoading(false);
});
}
}, [supplierId]);
// 校验姓名函数
const validateName = (name: string) => {
if (!name) {
return false;
}
// 姓名至少2个字符
return name.length >= 2;
};
// 校验身份证号函数
const validateIdCard = (idCard: string) => {
if (!idCard) {
return false;
}
// 18位身份证号正则表达式
const idCardRegex =
/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
return idCardRegex.test(idCard);
};
// 校验银行卡号函数
const validateBankCard = (bankCard: string) => {
if (!bankCard) {
return false;
}
// 银行卡号一般为16-19位数字
const bankCardRegex = /^\d{16,19}$/;
return bankCardRegex.test(bankCard);
};
// 校验手机号函数 (使用项目中已有的规则)
const validatePhone = (phone: string) => {
if (!phone) {
return false;
}
// 使用项目规范的手机号正则表达式
const phoneRegex = /^1[3456789]\d{9}$/;
return phoneRegex.test(phone);
};
// 处理姓名变化
const handleNameChange = (value: string) => {
setSupplierVO({
...supplierVO,
name: value,
});
// 校验并更新错误状态
const isValid = validateName(value);
setNameError(!isValid && value !== "");
if (isValid) {
setNameError(false);
}
// 清除姓名重复错误提示
setNameDuplicateError(false);
};
// 处理姓名失焦
const handleNameBlur = async (value: string) => {
if (value && !validateName(value)) {
setNameError(true);
return;
} else {
setNameError(false);
}
// 检查瓜农姓名是否在系统中存在
if (value) {
setLoading(true);
try {
const { data } = await business.supplier.checkSupplier({
supplierCheckQry: {
name: value,
},
});
if (data.success && data.data) {
// 系统中已存在该瓜农信息,设置姓名重复错误状态
setNameDuplicateError(true);
} else {
setNameDuplicateError(false);
}
} catch (error) {
console.error("检查瓜农信息失败", error);
setNameDuplicateError(false);
} finally {
setLoading(false);
}
}
};
// 处理身份证号变化
const handleIdCardChange = (value: string) => {
setSupplierVO({
...supplierVO,
idCard: value,
});
// 校验并更新错误状态
const isValid = validateIdCard(value);
setIdCardError(!isValid && value !== "");
if (isValid) {
setIdCardError(false);
}
};
// 处理身份证号失焦
const handleIdCardBlur = (value: string) => {
if (value && !validateIdCard(value)) {
setIdCardError(true);
} else {
setIdCardError(false);
}
};
// 处理银行卡号变化
const handleBankCardChange = (value: string) => {
setSupplierVO({
...supplierVO,
bankCard: value,
});
// 校验并更新错误状态
const isValid = validateBankCard(value);
setBankCardError(!isValid && value !== "");
if (isValid) {
setBankCardError(false);
}
};
// 处理银行卡号失焦
const handleBankCardBlur = (value: string) => {
if (value && !validateBankCard(value)) {
setBankCardError(true);
} else {
setBankCardError(false);
}
};
// 处理手机号变化
const handlePhoneChange = (value: string) => {
setSupplierVO({
...supplierVO,
phone: value,
});
// 校验并更新错误状态
const isValid = validatePhone(value);
setPhoneError(!isValid && value !== "");
if (isValid) {
setPhoneError(false);
}
};
// 处理手机号失焦
const handlePhoneBlur = (value: string) => {
if (value && !validatePhone(value)) {
setPhoneError(true);
} else {
setPhoneError(false);
}
};
const handleSave = async () => {
// 校验表单
const isNameValid = validateName(supplierVO.name || "");
const isIdCardValid = validateIdCard(supplierVO.idCard || "");
const isBankCardValid = validateBankCard(supplierVO.bankCard || "");
const isPhoneValid = validatePhone(supplierVO.phone || "");
setNameError(!isNameValid);
setIdCardError(!isIdCardValid);
setBankCardError(!isBankCardValid);
setPhoneError(!isPhoneValid);
// 检查是否已存在同名瓜农
let isDuplicate = false;
if (isNameValid) {
try {
setBtnLoading(true);
const { data: checkData } = await business.supplier.checkSupplier({
supplierCheckQry: {
name: supplierVO.name,
},
});
if (checkData.success && checkData.data) {
isDuplicate = true;
setNameDuplicateError(true);
}
} catch (error) {
console.error("检查瓜农信息失败", error);
} finally {
setBtnLoading(false);
}
}
const isValid =
isNameValid &&
isIdCardValid &&
isBankCardValid &&
isPhoneValid &&
!isDuplicate;
if (!isValid) {
if (isDuplicate) {
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "该瓜农已存在,请勿重复创建",
});
} else {
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请完善瓜农信息后再进行保存操作",
});
}
return;
}
// 创建新瓜农
try {
setBtnLoading(true);
const { data } = await business.supplier.createSupplier({
supplierId: supplierVO.supplierId,
name: supplierVO.name,
idCard: supplierVO.idCard,
phone: supplierVO.phone,
bankCard: supplierVO.bankCard,
wechatQr: supplierVO.wechatQr,
status: supplierVO.status,
});
if (data.success) {
Toast.show("toast", {
icon: "success",
title: "成功",
content: "瓜农创建成功",
});
setTimeout(() => {
Taro.navigateBack();
}, 1500);
} else {
Toast.show("toast", {
icon: "fail",
title: "失败",
content: data.errMessage || "创建失败",
});
}
} catch (error) {
Toast.show("toast", {
icon: "fail",
title: "错误",
content: "创建过程中发生错误",
});
console.error("创建瓜农失败:", error);
} finally {
setBtnLoading(false);
}
};
const handleWechatQrUpload = async () => {
await Taro.chooseImage({
count: 1,
sourceType: ["album", "camera"],
success: (res) => {
setLoading(true);
const file = res.tempFiles[0];
uploadFile(file.path)
.then(({ url }) => {
setSupplierVO({
...supplierVO,
wechatQr: url,
});
setLoading(false);
Toast.show("toast", {
title: "上传成功",
icon: "success",
content: "微信收款码已上传",
});
})
.catch((err) => {
Toast.show("toast", {
title: "上传失败",
icon: "fail",
content: err.message || "上传过程中发生错误",
});
setLoading(false);
});
},
fail: (err) => {
Toast.show("toast", {
title: "选择图片失败",
icon: "fail",
content: err.errMsg,
});
},
});
};
return (
<>
<View className="flex-1">
<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
className={"mr-1"}
name="circle-info"
color={"var(--color-blue-700)"}
size={18}
/>
<View className={"text-sm text-blue-700"}>
</View>
</View>
{/* 快捷工具 */}
<View className="flex gap-2">
<View className={"flex-1"}>
<Button
block
icon={<Icon name={"id-card"} size={28} color={"white"} />}
type={"primary"}
size={"xlarge"}
className="bg-primary flex flex-1 items-center justify-center text-white"
onClick={() => {
Taro.navigateTo({
url: buildUrl("/pages/public/camera/ocr", {
type: "idcard",
}),
complete: () => {
Taro.eventCenter.on("ocr", (res) => {
console.log("识别结果为:", res.result);
setSupplierVO({
...supplierVO,
name: res.result.name,
idCard: res.result.idCard,
});
Taro.eventCenter.off("ocr");
});
},
});
}}
>
<View></View>
</Button>
</View>
<View className={"flex-1"}>
<Button
block
icon={<Icon name={"credit-card"} size={28} color={"white"} />}
type={"primary"}
size={"xlarge"}
className="bg-primary flex flex-1 items-center justify-center text-white"
onClick={() => {
Taro.navigateTo({
url: buildUrl("/pages/public/camera/ocr", {
type: "bankcard",
}),
complete: () => {
Taro.eventCenter.on("ocr", (res) => {
console.log("识别结果为:", res.result);
setSupplierVO({
...supplierVO,
bankCard: res.result.number,
});
Taro.eventCenter.off("ocr");
});
},
});
}}
>
<View></View>
</Button>
</View>
</View>
{/* 瓜农信息 */}
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className="flex items-center justify-between">
<View className="text-primary text-base font-bold">
{supplierVO.name || "瓜农"}
</View>
</View>
<View className={"block text-sm font-normal text-[#000000]"}>
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${nameError || nameDuplicateError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon name="user" size={16} color="#999" className="mx-2" />
<Input
type="text"
placeholder="请输入姓名"
value={supplierVO.name}
onChange={(value) => handleNameChange(value)}
onBlur={() => handleNameBlur(supplierVO.name || "")}
className="flex-1"
/>
</View>
{nameError && (
<View className="mt-1 text-xs text-red-500">
{`姓名"${supplierVO.name}"至少2个字符`}
</View>
)}
{nameDuplicateError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
<View className="block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${idCardError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon name="id-card" size={16} color="#999" className="mx-2" />
<Input
type="idcard"
placeholder="请输入身份证号"
value={supplierVO.idCard}
onChange={(value) => handleIdCardChange(value)}
onBlur={() => handleIdCardBlur(supplierVO.idCard || "")}
className="flex-1"
/>
</View>
{idCardError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
<View className="block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${bankCardError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon
name="credit-card"
size={16}
color="#999"
className="mx-2"
/>
<Input
type="digit"
placeholder="请输入银行卡号"
value={supplierVO.bankCard}
onChange={(value) => handleBankCardChange(value)}
onBlur={() => handleBankCardBlur(supplierVO.bankCard || "")}
className="flex-1"
/>
</View>
{bankCardError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
<View className="block text-sm font-normal text-[#000000]">
</View>
<View
className={`flex h-10 w-full items-center rounded-md ${phoneError ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Icon name="phone" size={16} color="#999" className="mx-2" />
<Input
type="tel"
placeholder="请输入手机号码"
value={supplierVO.phone}
onChange={(value) => handlePhoneChange(value)}
onBlur={() => handlePhoneBlur(supplierVO.phone || "")}
className="flex-1"
/>
</View>
{phoneError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
</View>
{/* 若瓜农无法开发票,则可打款到微信 */}
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className="text-primary text-base font-bold">
{supplierVO.name || "瓜农"}
</View>
{supplierVO.wechatQr ? (
<View className="flex flex-col gap-2.5 rounded-lg bg-green-50 p-2.5">
<View className="flex items-center">
<View className="relative mr-3 h-20 w-20 overflow-hidden rounded-lg">
<Image
className="h-full w-full object-cover"
src={supplierVO.wechatQr}
/>
</View>
<View className="flex-1">
<View className="font-medium text-green-700">
</View>
<View className="text-sm text-green-600"></View>
<View className="text-neutral-darker mt-1 text-xs">
</View>
</View>
</View>
<View className="flex flex-row gap-2.5">
<View className={"flex-1"}>
<Button
type={"primary"}
size={"large"}
fill={"outline"}
block
onClick={handleWechatQrUpload}
>
<View></View>
</Button>
</View>
<View className={"flex-1"}>
<Button
type={"warning"}
size={"large"}
fill={"outline"}
block
onClick={(e) => {
setSupplierVO({
...supplierVO,
wechatQr: undefined,
});
Toast.show("toast", {
title: "删除成功",
icon: "success",
content: "微信收款码已删除",
});
e.stopPropagation();
}}
>
<View></View>
</Button>
</View>
</View>
</View>
) : (
<View
className={
"flex h-40 flex-1 flex-col items-center justify-center rounded-md border-2 border-dashed border-green-300 bg-green-50 p-2.5"
}
onClick={handleWechatQrUpload}
>
<Icon name={"camera"} size={24} />
<View className="text-sm font-medium text-green-700">
</View>
<View className="text-neutral-darker mt-1 text-xs">
</View>
<View className="mt-2 text-center text-xs text-gray-400">
</View>
</View>
)}
</View>
</View>
</View>
</View>
<View className="sticky bottom-0 z-10 bg-white">
<View className="flex justify-between gap-2 border-t border-gray-200 p-2.5">
<View className={"flex-1"}>
<Button
block
type="primary"
size={"xlarge"}
onClick={handleSave}
loading={btnLoading}
>
</Button>
</View>
</View>
<SafeArea position={"bottom"} />
</View>
</>
);
});