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

View File

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

View File

@ -6,8 +6,9 @@ import {
TextArea, TextArea,
} from "@nutui/nutui-react-taro"; } from "@nutui/nutui-react-taro";
import { CostPicker, Icon } from "@/components"; import { CostPicker, Icon } from "@/components";
import { ScrollView, View } from "@tarojs/components"; import { ScrollView, Text, View } from "@tarojs/components";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { generateShortId } from "@/utils";
interface IExpenseCostCreateProps { interface IExpenseCostCreateProps {
onFinish?: (expressCost: BusinessAPI.ExpenseCost) => void; onFinish?: (expressCost: BusinessAPI.ExpenseCost) => void;
@ -32,7 +33,6 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
const [formErrors, setFormErrors] = useState<{ const [formErrors, setFormErrors] = useState<{
costId?: boolean; costId?: boolean;
expenseAmount?: boolean; expenseAmount?: boolean;
remark?: boolean;
}>({}); }>({});
// 初始化表单数据 // 初始化表单数据
@ -55,7 +55,6 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
costId: !expressCost?.costId, costId: !expressCost?.costId,
expenseAmount: expenseAmount:
!expressCost?.expenseAmount || expressCost.expenseAmount <= 0, !expressCost?.expenseAmount || expressCost.expenseAmount <= 0,
remark: false, // 备注是可选的,不设为错误
}; };
setFormErrors(errors); setFormErrors(errors);
@ -75,7 +74,7 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
...expressCost, ...expressCost,
expenseCostId: editMode expenseCostId: editMode
? initialExpenseCost?.expenseCostId || expressCost.expenseCostId ? initialExpenseCost?.expenseCostId || expressCost.expenseCostId
: expressCost.expenseCostId || `EC_${Date.now()}`, : expressCost.expenseCostId || generateShortId(),
}; };
onFinish?.(finalExpenseCost); onFinish?.(finalExpenseCost);
@ -123,13 +122,16 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
) : ( ) : (
<> <>
<View className="block text-sm font-normal text-[#000000]"> <View className="block text-sm font-normal text-[#000000]">
<Text className="text-red-500">*</Text>
</View> </View>
<View <View
id={"target2"} 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"}`} className={`border-neutral-base flex flex-row items-center rounded-md ${formErrors.costId ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
> >
<CostPicker <CostPicker
params={{
type: "EXPENSE_TYPE",
}}
onFinish={(costVO) => { onFinish={(costVO) => {
setExpressCost((prev) => { setExpressCost((prev) => {
return { return {
@ -167,7 +169,7 @@ export default function ExpenseCostCreate(props: IExpenseCostCreateProps) {
)} )}
<View className={"text-neutral-darkest text-sm font-medium"}> <View className={"text-neutral-darkest text-sm font-medium"}>
<Text className="text-red-500">*</Text>
</View> </View>
<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"}`} 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, TextArea,
} from "@nutui/nutui-react-taro"; } from "@nutui/nutui-react-taro";
import { DealerPicker, Icon } from "@/components"; import { DealerPicker, Icon } from "@/components";
import { ScrollView, View } from "@tarojs/components"; import { ScrollView, Text, View } from "@tarojs/components";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { generateShortId } from "@/utils";
interface IExpenseProvisionCreateProps { interface IExpenseProvisionCreateProps {
onFinish?: (expenseProvision: BusinessAPI.ExpenseProvision) => void; onFinish?: (expenseProvision: BusinessAPI.ExpenseProvision) => void;
@ -33,9 +34,7 @@ export default function ExpenseProvisionCreate(
// 表单错误状态 // 表单错误状态
const [formErrors, setFormErrors] = useState<{ const [formErrors, setFormErrors] = useState<{
dealerName?: boolean;
provisionAmount?: boolean; provisionAmount?: boolean;
remark?: boolean;
}>({}); }>({});
// 初始化表单数据 // 初始化表单数据
@ -55,11 +54,9 @@ export default function ExpenseProvisionCreate(
const validateForm = () => { const validateForm = () => {
const errors = { const errors = {
dealerName: !expenseProvision?.dealerName,
provisionAmount: provisionAmount:
!expenseProvision?.provisionAmount || !expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0, expenseProvision.provisionAmount <= 0,
remark: false, // 备注是可选的,不设为错误
}; };
setFormErrors(errors); setFormErrors(errors);
@ -80,7 +77,7 @@ export default function ExpenseProvisionCreate(
expenseProvisionId: editMode expenseProvisionId: editMode
? initialExpenseProvision?.expenseProvisionId || ? initialExpenseProvision?.expenseProvisionId ||
expenseProvision.expenseProvisionId expenseProvision.expenseProvisionId
: expenseProvision.expenseProvisionId || `EP_${Date.now()}`, : expenseProvision.expenseProvisionId || generateShortId(),
}; };
onFinish?.(finalExpenseProvision); onFinish?.(finalExpenseProvision);
@ -117,22 +114,22 @@ export default function ExpenseProvisionCreate(
{editMode ? ( {editMode ? (
<> <>
<View className="block text-sm font-normal text-[#000000]"> <View className="block text-sm font-normal text-[#000000]">
</View> </View>
<View className="rounded-md bg-gray-100 p-3"> <View className="rounded-md bg-gray-100 p-3">
<View className="text-base font-medium text-gray-800"> <View className="text-base font-medium text-gray-800">
{expenseProvision?.dealerName || "未客户"} {expenseProvision?.dealerName || "未客户"}
</View> </View>
</View> </View>
</> </>
) : ( ) : (
<> <>
<View className="block text-sm font-normal text-[#000000]"> <View className="block text-sm font-normal text-[#000000]">
</View> </View>
<View <View
id={"target2"} 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 <DealerPicker
onFinish={(dealerVO) => { onFinish={(dealerVO) => {
@ -142,11 +139,6 @@ export default function ExpenseProvisionCreate(
dealerName: dealerVO.shortName, dealerName: dealerVO.shortName,
}; };
}); });
setFormErrors((prev) => ({
...prev,
dealerId: false,
}));
}} }}
trigger={ trigger={
<View <View
@ -162,14 +154,11 @@ export default function ExpenseProvisionCreate(
} }
/> />
</View> </View>
{formErrors.dealerName && (
<View className="mt-1 text-xs text-red-500"></View>
)}
</> </>
)} )}
<View className={"text-neutral-darkest text-sm font-medium"}> <View className={"text-neutral-darkest text-sm font-medium"}>
<Text className="text-red-500">*</Text>
</View> </View>
<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"}`} 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 block
type="primary" type="primary"
disabled={ disabled={
!expenseProvision?.dealerName ||
!expenseProvision?.provisionAmount || !expenseProvision?.provisionAmount ||
expenseProvision.provisionAmount <= 0 || expenseProvision.provisionAmount <= 0 ||
Object.values(formErrors).some((item) => item) Object.values(formErrors).some((item) => item)

View File

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

View File

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