ERPTurbo_Client/packages/app-client/src/components/expenses/ExpenseProvisionCreate.tsx
shenyifei 8562aed7d1 feat(expenses): 优化费用创建与计提功能
- 为 ExpenseCostCreate 和 ExpenseProvisionCreate 组件添加必填字段标识
- 使用 generateShortId 替代时间戳生成唯一 ID
- 移除备注字段的非必要校验逻辑
- 更新客户选择字段的显示文案与交互样式
- 在 ExpenseProvisionCreate 中移除客户名称必填限制
- 引入 Text 组件支持星号标注必填项
- 修复表单提交按钮的禁用条件判断逻辑
- 集成全局 loading 状态管理
- 实现费用记录保存成功后的提示反馈
- 将 DatePicker 替换为 Calendar 组件用于日期选择
- 为 CostPicker 添加 EXPENSE_TYPE 类型筛选参数
- 在 CostPageQry 类型中新增 name 字段定义
- 优化图标组件使用方式,替换原有 class 样式写法
2025-12-19 15:06:56 +08:00

269 lines
8.1 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, Text, View } from "@tarojs/components";
import { useEffect, useState } from "react";
import { generateShortId } from "@/utils";
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<{
provisionAmount?: boolean;
}>({});
// 初始化表单数据
useEffect(() => {
if (visible) {
if (editMode && initialExpenseProvision) {
// 编辑模式:回填数据
setExpenseProvision(initialExpenseProvision);
} else {
// 新增模式:清空数据
setExpenseProvision(undefined);
}
// 清空错误状态
setFormErrors({});
}
}, [visible, editMode, initialExpenseProvision]);
const validateForm = () => {
const errors = {
provisionAmount:
!expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0,
};
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 || generateShortId(),
};
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 border-4 border-gray-300`}
>
<DealerPicker
onFinish={(dealerVO) => {
setExpenseProvision((prev) => {
return {
...prev!,
dealerName: dealerVO.shortName,
};
});
}}
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>
</>
)}
<View className={"text-neutral-darkest text-sm font-medium"}>
<Text className="text-red-500">*</Text>
</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?.provisionAmount ||
expenseProvision.provisionAmount <= 0 ||
Object.values(formErrors).some((item) => item)
}
onClick={saveExpenseProvision}
>
{editMode ? "保存" : "添加"}
</Button>
</View>
</View>
<SafeArea position={"bottom"} />
</View>
</Popup>
</>
);
}