模板抽离复用

This commit is contained in:
liamzi
2024-12-26 18:24:05 +08:00
parent 0c7757de52
commit c5b2ae574b
3 changed files with 641 additions and 780 deletions

View File

@@ -0,0 +1,527 @@
import React, { useState, useEffect } 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';
import { supabase } from "@/config/supabase";
const { Text } = Typography;
const SectionList = ({
form,
isView,
formValues,
currentCurrency = 'CNY'
}) => {
const [editingSectionIndex, setEditingSectionIndex] = useState(null);
const [editingSectionName, setEditingSectionName] = useState('');
const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [availableSections, setAvailableSections] = useState([]);
const [loading, setLoading] = useState(false);
const [units, setUnits] = useState([]);
const [loadingUnits, setLoadingUnits] = useState(false);
// 内部计算方法
const calculateItemAmount = (quantity, price) => {
const safeQuantity = Number(quantity) || 0;
const safePrice = Number(price) || 0;
return safeQuantity * safePrice;
};
const calculateSectionTotal = (items = []) => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
return sum + calculateItemAmount(item.quantity, item.price);
}, 0);
};
const formatCurrency = (amount) => {
const CURRENCY_SYMBOLS = {
CNY: "¥",
TWD: "NT$",
USD: "$",
};
const safeAmount = Number(amount) || 0;
return `${CURRENCY_SYMBOLS[currentCurrency] || ""}${safeAmount.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
};
// 获取可用的小节模板
const fetchAvailableSections = async () => {
try {
setLoading(true);
const { data: sections, error } = await supabase
.from("resources")
.select("*")
.eq("type", "sections")
.order("created_at", { ascending: false });
if (error) throw error;
setAvailableSections(sections || []);
} catch (error) {
message.error("获取小节模板失败");
console.error(error);
} finally {
setLoading(false);
}
};
// 处理小节名称编辑
const handleSectionNameEdit = (sectionIndex, initialValue) => {
setEditingSectionIndex(sectionIndex);
setEditingSectionName(initialValue || '');
};
const handleSectionNameSave = () => {
if (!editingSectionName.trim()) {
message.error('请输入小节名称');
return;
}
const sections = form.getFieldValue('sections');
const newSections = [...sections];
newSections[editingSectionIndex] = {
...newSections[editingSectionIndex],
sectionName: editingSectionName.trim()
};
form.setFieldValue('sections', newSections);
setEditingSectionIndex(null);
setEditingSectionName('');
};
// 添加项目
const handleAddItem = (add) => {
add({
key: uuidv4(),
name: '',
description: '',
quantity: 1,
price: 0,
unit: ''
});
};
// 处理使用模板
const handleUseTemplate = (template, add) => {
const newSection = {
key: uuidv4(),
sectionName: template.attributes.name,
items: (template.attributes.items || []).map(item => ({
key: uuidv4(),
name: item.name || '',
description: item.description || '',
price: item.price || 0,
quantity: item.quantity || 1,
unit: item.unit || '',
})),
};
add(newSection);
setTemplateModalVisible(false);
message.success('套用模版成功');
};
// 处理创建自定义小节
const handleCreateCustom = (add, fieldsLength) => {
add({
key: uuidv4(),
sectionName: `服务类型 ${fieldsLength + 1}`,
items: [{
key: uuidv4(),
name: '',
description: '',
quantity: 1,
price: 0,
unit: ''
}]
});
setTemplateModalVisible(false);
};
// 获取单位列表
const fetchUnits = async () => {
setLoadingUnits(true);
try {
const { data: unitsData, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'units')
.order('created_at', { ascending: false });
if (error) throw error;
setUnits(unitsData || []);
} catch (error) {
message.error('获取单位列表失败');
console.error(error);
} finally {
setLoadingUnits(false);
}
};
// 在组件加载时获取单位列表
useEffect(() => {
fetchUnits();
}, []);
// 新增单位
const handleAddUnit = async (unitName) => {
try {
const { error } = await supabase
.from('resources')
.insert([{
type: 'units',
attributes: {
name: unitName
},
schema_version: 1
}]);
if (error) throw error;
message.success('新增单位成功');
fetchUnits();
return true;
} catch (error) {
message.error('新增单位失败');
console.error(error);
return false;
}
};
// 模板选择弹窗内容
const renderTemplateModalContent = (add, fieldsLength) => (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{availableSections.map(section => (
<div
key={section.id}
className="group relative bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 cursor-pointer"
onClick={() => handleUseTemplate(section, add)}
>
<div className="p-6">
<div className="text-center mb-4">
<h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors">
{section.attributes.name}
</h3>
<div className="text-sm text-gray-500 mt-1">
{section.attributes.items?.length || 0} 个项目
</div>
</div>
<div className="space-y-2 mt-4 border-t pt-4">
{(section.attributes.items || []).slice(0, 3).map((item, index) => (
<div key={index} className="flex justify-between items-center">
<span className="text-sm text-gray-600 truncate flex-1">{item.name}</span>
<span className="text-sm text-gray-500 ml-2">{formatCurrency(item.price)}</span>
</div>
))}
{(section.attributes.items || []).length > 3 && (
<div className="text-sm text-gray-500 text-center">
还有 {section.attributes.items.length - 3} 个项目...
</div>
)}
</div>
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<span className="text-sm text-gray-600">总金额</span>
<span className="text-base font-medium text-blue-500">
{formatCurrency(
(section.attributes.items || []).reduce(
(sum, item) => sum + (item.price * (item.quantity || 1) || 0),
0
)
)}
</span>
</div>
</div>
</div>
))}
</div>
<Divider />
<div className="flex justify-center">
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => handleCreateCustom(add, fieldsLength)}
className="w-1/3 border-2"
>
自定义小节
</Button>
</div>
</div>
);
return (
<>
<Form.List name="sections">
{(fields, { add, remove }) => (
<>
<div className="space-y-4">
{fields.map((field, sectionIndex) => (
<Card
key={field.key}
className="shadow-sm rounded-lg"
type="inner"
title={
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{editingSectionIndex === sectionIndex ? (
<div className="flex items-center gap-2">
<Input
value={editingSectionName}
onChange={(e) => setEditingSectionName(e.target.value)}
onPressEnter={handleSectionNameSave}
autoFocus
className="w-48"
/>
<Button
type="link"
icon={<CheckOutlined />}
onClick={handleSectionNameSave}
className="text-green-500"
/>
<Button
type="link"
icon={<CloseOutlined />}
onClick={() => {
setEditingSectionIndex(null);
setEditingSectionName('');
}}
className="text-red-500"
/>
</div>
) : (
<div className="flex items-center gap-2">
<span className="w-1 h-4 bg-purple-500 rounded-full" />
<Text strong className="text-lg">
{form.getFieldValue(['sections', sectionIndex, 'sectionName'])
|| `服务类型 ${sectionIndex + 1}`}
</Text>
{!isView && (
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleSectionNameEdit(
sectionIndex,
form.getFieldValue(['sections', sectionIndex, 'sectionName'])
)}
className="text-gray-400 hover:text-blue-500"
/>
)}
</div>
)}
</div>
{!isView && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => remove(field.name)}
/>
)}
</div>
}
>
{/* 项目列表 */}
<Form.List name={[field.name, "items"]}>
{(itemFields, { add: addItem, remove: removeItem }) => (
<>
{/* 表头 */}
<div className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-2 text-gray-500 px-2">
<div>项目明细</div>
<div>描述/备注</div>
<div>单位</div>
<div className="text-center">数量</div>
<div className="text-center">单价</div>
<div className="text-right">小计</div>
<div></div>
</div>
{/* 项目列表 */}
{itemFields.map((itemField, itemIndex) => (
<div
key={itemField.key}
className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-4 items-start"
>
<Form.Item
{...itemField}
name={[itemField.name, "name"]}
className="!mb-0"
>
<Input placeholder="服务项目名称" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "description"]}
className="!mb-0"
>
<Input placeholder="请输入描述/备注" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Select
placeholder="选择单位"
loading={loadingUnits}
showSearch
allowClear
options={units.map(unit => ({
label: unit.attributes.name,
value: unit.attributes.name
}))}
onDropdownVisibleChange={(open) => {
if (open) fetchUnits();
}}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: '8px 0' }} />
<Select.Option value="ADD_NEW">
<Button
type="text"
icon={<PlusOutlined />}
block
onClick={(e) => {
e.stopPropagation();
Modal.confirm({
title: '新增单位',
content: (
<Input
placeholder="请输入单位名称"
onChange={(e) => {
Modal.confirm.update({
okButtonProps: {
disabled: !e.target.value.trim()
}
});
}}
ref={(input) => {
if (input) {
setTimeout(() => input.focus(), 100);
}
}}
/>
),
onOk: async (close) => {
const unitName = document.querySelector('.ant-modal-content input').value.trim();
if (await handleAddUnit(unitName)) {
const currentItems = form.getFieldValue(['sections', field.name, 'items']);
currentItems[itemField.name].unit = unitName;
form.setFieldValue(['sections', field.name, 'items'], currentItems);
close();
}
}
});
}}
>
新增单位
</Button>
</Select.Option>
</>
)}
/>
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "quantity"]}
className="!mb-0"
>
<InputNumber placeholder="数量" min={0} className="w-full" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "price"]}
className="!mb-0"
>
<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>
{!isView && itemFields.length > 1 && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeItem(itemField.name)}
className="flex items-center justify-center"
/>
)}
</div>
))}
{!isView && (
<Button
type="dashed"
onClick={() => handleAddItem(addItem)}
icon={<PlusOutlined />}
className="w-full hover:border-blue-400 hover:text-blue-500 mb-4"
>
添加服务项目
</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>
</>
)}
</Form.List>
</Card>
))}
</div>
{!isView && (
<div className="mt-6 flex justify-center">
<Button
type="dashed"
onClick={() => {
setTemplateModalVisible(true);
fetchAvailableSections();
}}
icon={<PlusOutlined />}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
>
新建模块
</Button>
</div>
)}
<Modal
title={<h3 className="text-lg font-medium">选择模版</h3>}
open={templateModalVisible}
onCancel={() => setTemplateModalVisible(false)}
footer={null}
width={800}
closeIcon={<CloseOutlined className="text-gray-500" />}
>
{renderTemplateModalContent(add, fields.length)}
</Modal>
</>
)}
</Form.List>
</>
);
};
export default SectionList;