- 添加费用类型选择器组件 CostPicker,支持搜索和选择费用类型 - 实现费用录入卡片 ExpenseCostCard,支持编辑和删除操作 - 创建费用录入弹窗 ExpenseCostCreate,包含费用类型、金额和备注字段 - 开发费用列表组件 ExpenseCostList,展示已录入费用并计算合计金额 - 实现计提记录卡片 ExpenseProvisionCard,支持编辑和删除计提信息 - 添加计提录入弹窗 ExpenseProvisionCreate,集成客户选择和金额输入 - 创建计提列表组件 ExpenseProvisionList,按客户分组展示计提记录 - 更新图标组件 Icon,新增 trash-can、pen、chevron-up 和 building 图标 - 导出所有费用相关组件,便于在其他模块中复用
281 lines
8.6 KiB
TypeScript
281 lines
8.6 KiB
TypeScript
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>
|
||
</>
|
||
);
|
||
}
|