feat(expenses): 优化费用创建与计提功能

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

View File

@ -7,10 +7,11 @@ import { business } from "@/services";
interface ICostPickerProps {
onFinish: (costVO: BusinessAPI.CostVO) => void;
trigger: React.ReactNode;
params?: BusinessAPI.CostPageQry;
}
export default function CostPicker(props: ICostPickerProps) {
const { onFinish, trigger } = props;
const { onFinish, trigger, params } = props;
const [visible, setVisible] = useState(false);
const [costVO, setCostVO] = useState<BusinessAPI.CostVO>();
@ -28,6 +29,7 @@ export default function CostPicker(props: ICostPickerProps) {
costListQry: {
name: value || undefined,
status: true,
...params,
},
});

View File

@ -25,8 +25,8 @@ export default function ExpenseCostCard(props: IExpenseCostCardProps) {
>
<View className="flex-1">
<View className="flex items-center">
<View className="bg-primary bg-opacity-10 mr-2 flex h-8 w-8 items-center justify-center rounded-full">
<i className="fas fa-receipt text-primary text-sm"></i>
<View className="bg-primary/10 mr-2 flex h-8 w-8 items-center justify-center rounded-full">
<Icon name="receipt" size={18} />
</View>
<View>
<View className="text-neutral-darkest text-sm font-medium">

View File

@ -6,8 +6,9 @@ import {
TextArea,
} from "@nutui/nutui-react-taro";
import { CostPicker, Icon } from "@/components";
import { ScrollView, View } from "@tarojs/components";
import { ScrollView, Text, View } from "@tarojs/components";
import { useEffect, useState } from "react";
import { generateShortId } from "@/utils";
interface IExpenseCostCreateProps {
onFinish?: (expressCost: BusinessAPI.ExpenseCost) => void;
@ -32,7 +33,6 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
const [formErrors, setFormErrors] = useState<{
costId?: boolean;
expenseAmount?: boolean;
remark?: boolean;
}>({});
// 初始化表单数据
@ -55,7 +55,6 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
costId: !expressCost?.costId,
expenseAmount:
!expressCost?.expenseAmount || expressCost.expenseAmount <= 0,
remark: false, // 备注是可选的,不设为错误
};
setFormErrors(errors);
@ -75,7 +74,7 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
...expressCost,
expenseCostId: editMode
? initialExpenseCost?.expenseCostId || expressCost.expenseCostId
: expressCost.expenseCostId || `EC_${Date.now()}`,
: expressCost.expenseCostId || generateShortId(),
};
onFinish?.(finalExpenseCost);
@ -123,13 +122,16 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
) : (
<>
<View className="block text-sm font-normal text-[#000000]">
<Text className="text-red-500">*</Text>
</View>
<View
id={"target2"}
className={`border-neutral-base flex flex-row items-center rounded-md ${formErrors.costId ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
>
<CostPicker
params={{
type: "EXPENSE_TYPE",
}}
onFinish={(costVO) => {
setExpressCost((prev) => {
return {
@ -167,7 +169,7 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
)}
<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.expenseAmount ? "border-4 border-red-500" : "border-4 border-gray-300"}`}

View File

@ -6,8 +6,9 @@ import {
TextArea,
} from "@nutui/nutui-react-taro";
import { DealerPicker, Icon } from "@/components";
import { ScrollView, View } from "@tarojs/components";
import { ScrollView, Text, View } from "@tarojs/components";
import { useEffect, useState } from "react";
import { generateShortId } from "@/utils";
interface IExpenseProvisionCreateProps {
onFinish?: (expenseProvision: BusinessAPI.ExpenseProvision) => void;
@ -33,9 +34,7 @@ export default function ExpenseProvisionCreate(
// 表单错误状态
const [formErrors, setFormErrors] = useState<{
dealerName?: boolean;
provisionAmount?: boolean;
remark?: boolean;
}>({});
// 初始化表单数据
@ -55,11 +54,9 @@ export default function ExpenseProvisionCreate(
const validateForm = () => {
const errors = {
dealerName: !expenseProvision?.dealerName,
provisionAmount:
!expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0,
remark: false, // 备注是可选的,不设为错误
};
setFormErrors(errors);
@ -80,7 +77,7 @@ export default function ExpenseProvisionCreate(
expenseProvisionId: editMode
? initialExpenseProvision?.expenseProvisionId ||
expenseProvision.expenseProvisionId
: expenseProvision.expenseProvisionId || `EP_${Date.now()}`,
: expenseProvision.expenseProvisionId || generateShortId(),
};
onFinish?.(finalExpenseProvision);
@ -117,22 +114,22 @@ export default function ExpenseProvisionCreate(
{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 || "未客户"}
{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"}`}
className={`border-neutral-base flex flex-row items-center rounded-md border-4 border-gray-300`}
>
<DealerPicker
onFinish={(dealerVO) => {
@ -142,11 +139,6 @@ export default function ExpenseProvisionCreate(
dealerName: dealerVO.shortName,
};
});
setFormErrors((prev) => ({
...prev,
dealerId: false,
}));
}}
trigger={
<View
@ -162,14 +154,11 @@ export default function ExpenseProvisionCreate(
}
/>
</View>
{formErrors.dealerName && (
<View className="mt-1 text-xs text-red-500"></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"}`}
@ -261,7 +250,6 @@ export default function ExpenseProvisionCreate(
block
type="primary"
disabled={
!expenseProvision?.dealerName ||
!expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0 ||
Object.values(formErrors).some((item) => item)

View File

@ -2,12 +2,12 @@ import { useShareAppMessage } from "@tarojs/taro";
import hocAuth from "@/hocs/auth";
import { CommonComponent } from "@/types/typings";
import { View } from "@tarojs/components";
import { ExpenseProvisionList, Icon } from "@/components";
import { ExpenseCostList, ExpenseProvisionList, Icon } from "@/components";
import { useEffect, useRef, useState } from "react";
import { Button, DatePicker, Dialog, SafeArea } from "@nutui/nutui-react-taro";
import { Button, Calendar, Dialog, SafeArea, Toast } from "@nutui/nutui-react-taro";
import dayjs from "dayjs";
import { business } from "@/services";
import ExpenseCostList from "../../components/expenses/ExpenseCostList";
import { globalStore } from "@/store/global-store";
export default hocAuth(function Page(props: CommonComponent) {
const { shareOptions } = props;
@ -17,7 +17,7 @@ export default hocAuth(function Page(props: CommonComponent) {
);
const [expenseRecord, setExpenseRecord] =
useState<BusinessAPI.ExpenseRecordVO>();
const [loading, setLoading] = useState(true);
const { setLoading } = globalStore((state: any) => state);
const [show, setShow] = useState(false);
@ -98,14 +98,21 @@ export default hocAuth(function Page(props: CommonComponent) {
try {
// 这里添加实际的保存逻辑
console.log("保存数据:", expenseRecord);
// await business.expenseRecord.save(expenseRecord);
const {
data: { success },
} = await business.expenseRecord.createExpenseRecord({
...expenseRecord,
recordDate: currentDate,
});
if (success) {
// 保存成功后更新原始数据
setOriginalData(JSON.parse(JSON.stringify(expenseRecord)));
setHasUnsavedChanges(false);
lastSaveTime.current = Date.now();
}
return true;
return success;
} catch (error) {
console.error("保存数据失败:", error);
return false;
@ -161,14 +168,6 @@ export default hocAuth(function Page(props: CommonComponent) {
}
};
if (loading) {
return (
<View className="flex items-center justify-center py-8">
<View className="text-gray-500">...</View>
</View>
);
}
function onchange(expenseRecord: BusinessAPI.ExpenseRecordVO) {
setExpenseRecord((prev) => {
const totalProvision =
@ -291,7 +290,11 @@ export default hocAuth(function Page(props: CommonComponent) {
onClick={async () => {
const saveSuccess = await saveCurrentData();
if (saveSuccess) {
console.log("保存成功");
Toast.show("toast", {
icon: "success",
title: "提示",
content: "保存成功",
});
}
}}
disabled={!hasUnsavedChanges}
@ -303,16 +306,13 @@ export default hocAuth(function Page(props: CommonComponent) {
<SafeArea position="bottom" />
</View>
<DatePicker
title="日期选择"
type="date"
<Calendar
visible={show}
value={new Date(currentDate)}
showChinese
defaultValue={dayjs(currentDate).format("YYYY-MM-DD")}
startDate={"2025-01-01"}
onClose={() => setShow(false)}
threeDimensional={false}
onConfirm={(_, values) => {
const newDate = dayjs(values.join("-")).format("YYYY-MM-DD");
onConfirm={(data) => {
const newDate = dayjs(data[3]).format("YYYY-MM-DD");
// 如果没有未保存变更,直接切换日期
if (!hasUnsavedChanges) {
@ -325,6 +325,28 @@ export default hocAuth(function Page(props: CommonComponent) {
}}
/>
{/*<DatePicker*/}
{/* title="日期选择"*/}
{/* type="date"*/}
{/* visible={show}*/}
{/* value={new Date(currentDate)}*/}
{/* showChinese*/}
{/* onClose={() => setShow(false)}*/}
{/* threeDimensional={false}*/}
{/* onConfirm={(_, values) => {*/}
{/* const newDate = dayjs(values.join("-")).format("YYYY-MM-DD");*/}
{/* // 如果没有未保存变更,直接切换日期*/}
{/* if (!hasUnsavedChanges) {*/}
{/* setCurrentDate(newDate);*/}
{/* } else {*/}
{/* // 有未保存变更时,保存待切换日期,显示确认对话框*/}
{/* setPendingDate(newDate);*/}
{/* setConfirmVisible(true);*/}
{/* }*/}
{/* }}*/}
{/*/>*/}
{/* 日期切换确认对话框 */}
<Dialog
visible={confirmVisible}

View File

@ -1064,6 +1064,8 @@ declare namespace BusinessAPI {
| "LOGISTICS_TYPE";
/** 费用归属0_无归属1_工头2_产地3_司机 */
belong?: "NONE_TYPE" | "WORKER_TYPE" | "PRODUCTION_TYPE" | "DRIVER_TYPE";
/** 费用名称 */
name?: string;
};
type CostPageQry = {