feat:服务模版 抽离

This commit is contained in:
‘Liammcl’
2024-12-27 01:10:16 +08:00
parent bde0a8fd65
commit b9ea7218e3
16 changed files with 1078 additions and 883 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { Form, Input, InputNumber, Button, Card, Typography, Modal, message, Divider, Select } from 'antd';
import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { v4 as uuidv4 } from 'uuid';
@@ -247,12 +247,53 @@ const SectionList = ({
onClick={() => handleCreateCustom(add, fieldsLength)}
className="w-1/3 border-2"
>
自定义小节
自定义模块
</Button>
</div>
</div>
);
// 修改项目小计的计算,将其封装为 memo 组件
const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => {
const subtotal = useMemo(() => {
const safeQuantity = Number(quantity) || 0;
const safePrice = Number(price) || 0;
return safeQuantity * safePrice;
}, [quantity, price]);
return (
<div className="text-right">
<span className="text-gray-500">
{formatCurrency(subtotal, currentCurrency)}
</span>
</div>
);
});
// 修改小节总计的计算,将其封装为 memo 组件
const SectionTotal = React.memo(({ items, currentCurrency }) => {
const total = useMemo(() => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
const safeQuantity = Number(item.quantity) || 0;
const safePrice = Number(item.price) || 0;
return sum + (safeQuantity * safePrice);
}, 0);
}, [items]);
return (
<div className="flex justify-end border-t pt-4">
<span className="text-gray-500">
小计总额
<span className="text-blue-500 font-medium ml-2">
{formatCurrency(total, currentCurrency)}
</span>
</span>
</div>
);
});
return (
<>
<Form.List name="sections">
@@ -439,16 +480,11 @@ const SectionList = ({
>
<InputNumber placeholder="单价" min={0} className="w-full" />
</Form.Item>
<div className="text-right">
<span className="text-gray-500">
{formatCurrency(
calculateItemAmount(
formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity,
formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price
)
)}
</span>
</div>
<ItemSubtotal
quantity={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity}
price={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price}
currentCurrency={currentCurrency}
/>
{!isView && itemFields.length > 1 && (
<Button
type="text"
@@ -472,18 +508,10 @@ const SectionList = ({
</Button>
)}
<div className="flex justify-end border-t pt-4">
<span className="text-gray-500">
小计总额
<span className="text-blue-500 font-medium ml-2">
{formatCurrency(
calculateSectionTotal(
formValues?.sections?.[sectionIndex]?.items
)
)}
</span>
</span>
</div>
<SectionTotal
items={formValues?.sections?.[sectionIndex]?.items}
currentCurrency={currentCurrency}
/>
</>
)}
</Form.List>

View File

@@ -0,0 +1,55 @@
import React from 'react'
import { Modal, Card, Space } from 'antd';
import { FileTextOutlined, ProjectOutlined, CheckSquareOutlined } from '@ant-design/icons';
const TEMPLATE_TYPES = [
{
key: 'quotation',
title: '报价单模板',
icon: <FileTextOutlined className="text-2xl text-blue-500" />,
description: '创建标准化的报价单模板,包含服务项目、价格等信息'
},
{
key: 'project',
title: '专案模板',
icon: <ProjectOutlined className="text-2xl text-green-500" />,
description: '创建专案流程模板,包含项目阶段、时间线等信息'
},
{
key: 'task',
title: '任务模板',
icon: <CheckSquareOutlined className="text-2xl text-purple-500" />,
description: '创建标准任务模板,包含检查项、执行步骤等信息'
}
];
const TemplateTypeModal = ({ open, onClose, onSelect }) => {
return (
<Modal
title="选择模板类型"
open={open}
onCancel={onClose}
footer={null}
width={800}
>
<Space className="w-full" size="large">
{TEMPLATE_TYPES.map(type => (
<Card
key={type.key}
hoverable
className="flex-1 cursor-pointer transition-all hover:shadow-lg"
onClick={() => onSelect(type.key)}
>
<div className="text-center space-y-4">
{type.icon}
<h3 className="text-lg font-medium">{type.title}</h3>
<p className="text-gray-500 text-sm">{type.description}</p>
</div>
</Card>
))}
</Space>
</Modal>
);
};
export default TemplateTypeModal;