ERPTurbo_Client/packages/app-client/src/components/expenses/ExpenseProvisionCreate.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

281 lines
8.6 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 {
Button,
Input,
Popup,
SafeArea,
TextArea,
} from "@nutui/nutui-react-taro";
import { DealerPicker, Icon } from "@/components";
import { ScrollView, View } from "@tarojs/components";
import { useEffect, useState } from "react";
interface IExpenseProvisionCreateProps {
onFinish?: (expenseProvision: BusinessAPI.ExpenseProvision) => void;
visible: boolean;
onClose: () => void;
editMode?: boolean;
expenseProvision?: BusinessAPI.ExpenseProvision;
}
export default function ExpenseProvisionCreate(
props: IExpenseProvisionCreateProps,
) {
const {
onFinish,
visible,
onClose,
editMode = false,
expenseProvision: initialExpenseProvision,
} = props;
const [expenseProvision, setExpenseProvision] =
useState<BusinessAPI.ExpenseProvision>();
// 表单错误状态
const [formErrors, setFormErrors] = useState<{
dealerName?: boolean;
provisionAmount?: boolean;
remark?: boolean;
}>({});
// 初始化表单数据
useEffect(() => {
if (visible) {
if (editMode && initialExpenseProvision) {
// 编辑模式:回填数据
setExpenseProvision(initialExpenseProvision);
} else {
// 新增模式:清空数据
setExpenseProvision(undefined);
}
// 清空错误状态
setFormErrors({});
}
}, [visible, editMode, initialExpenseProvision]);
const validateForm = () => {
const errors = {
dealerName: !expenseProvision?.dealerName,
provisionAmount:
!expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0,
remark: false, // 备注是可选的,不设为错误
};
setFormErrors(errors);
// 检查是否有错误
return !Object.values(errors).some((error) => error);
};
const saveExpenseProvision = async () => {
if (!validateForm()) {
return;
}
if (expenseProvision) {
// 编辑模式下保留原有ID新增模式下生成新ID
const finalExpenseProvision: BusinessAPI.ExpenseProvision = {
...expenseProvision,
expenseProvisionId: editMode
? initialExpenseProvision?.expenseProvisionId ||
expenseProvision.expenseProvisionId
: expenseProvision.expenseProvisionId || `EP_${Date.now()}`,
};
onFinish?.(finalExpenseProvision);
}
// 重置表单
setExpenseProvision(undefined);
setFormErrors({});
onClose();
};
return (
<>
<Popup
duration={150}
style={{
minHeight: "auto",
}}
visible={visible}
className={"flex flex-col"}
position="bottom"
title={editMode ? "编辑计提" : "添加计提"}
onClose={() => {
onClose();
}}
onOverlayClick={() => {
onClose();
}}
lockScroll
>
<ScrollView scrollY className="h-96">
<View className={"flex flex-col gap-3 p-2.5"}>
{/* 编辑模式下客户名称不可修改 */}
{editMode ? (
<>
<View className="block text-sm font-normal text-[#000000]">
</View>
<View className="rounded-md bg-gray-100 p-3">
<View className="text-base font-medium text-gray-800">
{expenseProvision?.dealerName || "未知客户"}
</View>
</View>
</>
) : (
<>
<View className="block text-sm font-normal text-[#000000]">
</View>
<View
id={"target2"}
className={`border-neutral-base flex flex-row items-center rounded-md ${formErrors.dealerName ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<DealerPicker
onFinish={(dealerVO) => {
setExpenseProvision((prev) => {
return {
...prev!,
dealerName: dealerVO.shortName,
};
});
setFormErrors((prev) => ({
...prev,
dealerId: false,
}));
}}
trigger={
<View
className={
"flex flex-1 flex-row items-center justify-between px-5"
}
>
<View className={"text-sm"}>
{expenseProvision?.dealerName || "请选择客户"}
</View>
<Icon name={"chevron-down"} />
</View>
}
/>
</View>
{formErrors.dealerName && (
<View className="mt-1 text-xs text-red-500"></View>
)}
</>
)}
<View className={"text-neutral-darkest text-sm font-medium"}>
</View>
<View
className={`border-neutral-base flex flex-row items-center rounded-md ${formErrors.provisionAmount ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<Input
className={"placeholder:text-neutral-dark"}
type={"digit"}
value={expenseProvision?.provisionAmount?.toString() || ""}
placeholder="请输入计提金额"
onChange={(value) => {
const num = Number(value);
if (!Number.isNaN(num) && num >= 0) {
setExpenseProvision((prev) => {
return {
...prev!,
provisionAmount: num,
};
});
// 实时清除金额错误状态
if (num > 0) {
setFormErrors((prev) => ({
...prev,
provisionAmount: false,
}));
}
}
}}
/>
</View>
{formErrors.provisionAmount && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
<View className={"text-neutral-darkest text-sm font-medium"}>
</View>
<View
className={
"border-neutral-base flex flex-row items-center rounded-md border border-solid"
}
>
<TextArea
className={"flex-1"}
placeholder="请输入备注信息"
rows={4}
maxLength={200}
showCount
value={expenseProvision?.remark || ""}
onChange={(value) => {
// 清除错误状态
if (expenseProvision?.remark && value.trim()) {
setFormErrors((prev) => ({
...prev,
remark: false,
}));
}
setExpenseProvision((prev: any) => {
return {
...prev!,
remark: value,
};
});
}}
/>
</View>
</View>
</ScrollView>
<View className={"flex w-full flex-col bg-white"}>
<View className={"flex flex-row gap-2 p-3"}>
<View className={"flex-1"}>
<Button
size={"large"}
block
type="default"
onClick={() => {
onClose();
}}
>
</Button>
</View>
<View className={"flex-1"}>
<Button
size={"large"}
block
type="primary"
disabled={
!expenseProvision?.dealerName ||
!expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0 ||
Object.values(formErrors).some((item) => item)
}
onClick={saveExpenseProvision}
>
{editMode ? "保存" : "添加"}
</Button>
</View>
</View>
<SafeArea position={"bottom"} />
</View>
</Popup>
</>
);
}