quata模块完成

This commit is contained in:
‘Liammcl’
2024-12-22 23:20:20 +08:00
parent df0aa520ca
commit 98eb405cc5
14 changed files with 2822 additions and 609 deletions

View File

@@ -9,16 +9,20 @@ import {
Typography,
message,
Select,
Modal,
Divider,
Popconfirm,
} from "antd";
import {
PlusOutlined,
DeleteOutlined,
ArrowLeftOutlined,
EditOutlined,
CloseOutlined,
CheckOutlined,
} from "@ant-design/icons";
import { useNavigate, useParams, useLocation } from "react-router-dom";
import { supabase } from "@/config/supabase";
const { Title, Text } = Typography;
const ServiceForm = () => {
@@ -34,11 +38,17 @@ const ServiceForm = () => {
sections: [{ items: [{}] }],
currency: "CNY"
});
const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [categories, setCategories] = useState([]);
const [units, setUnits] = useState([]);
useEffect(() => {
if (id) {
fetchServiceTemplate();
}
fetchAvailableSections();
fetchCategories();
fetchUnits();
}, [id]);
const fetchServiceTemplate = async () => {
@@ -54,6 +64,7 @@ const ServiceForm = () => {
const formData = {
templateName: data.attributes.templateName,
description: data.attributes.description,
category:data.attributes.category.map(v=>v.id),
sections: data.attributes.sections,
};
form.setFieldsValue(formData);
@@ -69,15 +80,60 @@ const ServiceForm = () => {
const fetchAvailableSections = async () => {
try {
const { data: sections, error } = await supabase
.from("resources")
.select("*")
.eq("type", "serviceSection");
.from('resources')
.select('*')
.eq('type', 'sections')
.order('created_at', { ascending: false });
if (error) throw error;
setAvailableSections(sections);
setAvailableSections(sections || []);
} catch (error) {
console.error("获取服务小节失败:", error);
message.error("获取服务小节失败");
message.error('获取小节模版失败');
console.error(error);
}
};
const fetchCategories = async () => {
try {
const { data: categoriesData, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'categories')
.order('created_at', { ascending: false });
if (error) throw error;
const formattedCategories = (categoriesData || []).map(category => ({
value:category.id,
label: category.attributes.name
}));
setCategories(formattedCategories);
} catch (error) {
message.error('获取分类数据失败');
console.error(error);
}
};
const fetchUnits = async () => {
try {
const { data: unitsData, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'units')
.order('created_at', { ascending: false });
if (error) throw error;
const formattedUnits = (unitsData || []).map(unit => ({
value: unit.attributes.name,
label: unit.attributes.name
}));
setUnits(formattedUnits);
} catch (error) {
console.error('获取单位数据失败:', error);
message.error('获取单位数据失败');
}
};
@@ -92,14 +148,20 @@ const ServiceForm = () => {
}, 0)
);
}, 0);
const categoryData = values.category.map(categoryId => {
const category = categories.find(c => c.value === categoryId);
return {
id: categoryId,
name: category.label
};
});
const serviceData = {
type: "serviceTemplate",
attributes: {
templateName: values.templateName,
description: values.description,
sections: values.sections,
category: values.category || [], // 添加 category 字段
category: categoryData,
totalAmount,
},
};
@@ -241,6 +303,243 @@ const ServiceForm = () => {
setEditingSectionName('');
};
// 使用选中的模版
const handleUseTemplate = (template) => {
const currentSections = form.getFieldValue('sections') || [];
// 确保所有必要的字段都存在
const newSection = {
sectionName: template.attributes.name,
items: (template.attributes.items || []).map(item => ({
name: item.name || '',
description: item.description || '',
price: item.price || 0,
quantity: item.quantity || 1,
unit: item.unit || '',
}))
};
const newSections = [...currentSections, newSection];
// 更新表单值
form.setFieldValue('sections', newSections);
// 更新 formValues 以触发金额计算
setFormValues(prev => ({
...prev,
sections: newSections
}));
setTemplateModalVisible(false);
message.success('套用模版成功');
};
// 创建自定义小节
const handleCreateCustom = () => {
const currentSections = form.getFieldValue('sections') || [];
const newSection = {
sectionName: `服务类型 ${currentSections.length + 1}`,
items: [{ name: '', description: '', price: 0, quantity: 1, unit: '' }]
};
const newSections = [...currentSections, newSection];
// 更新表单值
form.setFieldValue('sections', newSections);
// 更新 formValues 以触发金额计算
setFormValues(prev => ({
...prev,
sections: newSections
}));
setTemplateModalVisible(false);
};
// 修改新增小节按钮的点击事件处理
const handleAddSection = () => {
setTemplateModalVisible(true);
fetchAvailableSections();
};
// 修改移除小节的处理方法
const handleRemoveSection = (sectionIndex) => {
const currentSections = form.getFieldValue('sections') || [];
const newSections = currentSections.filter((_, index) => index !== sectionIndex);
// 更新表单值
form.setFieldValue('sections', newSections);
// 更新 formValues 以触发金额计算
setFormValues(prev => ({
...prev,
sections: newSections
}));
message.success('删除成功');
};
// 优化模版选择弹窗内容
const renderTemplateModalContent = () => (
<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 dark:bg-gray-800 rounded-lg shadow-sm
border border-gray-200 dark:border-gray-700 hover:shadow-lg
transition-all duration-300 cursor-pointer"
onClick={() => handleUseTemplate(section)}
>
<div className="p-6">
{/* 标题和项目数量 */}
<div className="text-center mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100
group-hover:text-blue-500 transition-colors">
{section.attributes.name}
</h3>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{section.attributes.items?.length || 0} 个项目
</div>
</div>
{/* 项目列表预览 */}
<div className="space-y-2 mt-4 border-t dark:border-gray-700 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 dark:text-gray-300 truncate flex-1">
{item.name}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
{formatCurrency(item.price)}
</span>
</div>
))}
{(section.attributes.items || []).length > 3 && (
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
还有 {section.attributes.items.length - 3} 个项目...
</div>
)}
</div>
{/* 小节总金额 */}
<div className="mt-4 pt-4 border-t dark:border-gray-700 flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-300">总金额</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 className="absolute inset-0 border-2 border-transparent
group-hover:border-blue-500 rounded-lg transition-colors duration-300"
/>
{/* 选择指示器 */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100
transition-opacity duration-300">
<Button
type="primary"
size="small"
className="flex items-center gap-1"
icon={<PlusOutlined />}
>
套用
</Button>
</div>
</div>
))}
</div>
{availableSections.length === 0 && (
<div className="text-center py-8">
<div className="text-gray-500 dark:text-gray-400">
暂无可用模版
</div>
</div>
)}
<Divider className="dark:border-gray-700" />
<div className="flex justify-center">
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={handleCreateCustom}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400
transition-all duration-300"
>
自定义小节
</Button>
</div>
</div>
);
const renderUnitSelect = (itemField, sectionIndex, itemIndex) => {
const addUnit = async (unitName) => {
try {
const { error } = await supabase
.from('resources')
.insert([{
type: 'units',
attributes: {
name: unitName
},
schema_version: 1
}]);
if (error) throw error;
message.success('新增单位成功');
await fetchUnits();
// 自动选中新添加的单位
form.setFieldValue(['sections', sectionIndex, 'items', itemIndex, 'unit'], unitName);
} catch (error) {
message.error('新增单位失败');
console.error(error);
}
};
return (
<Form.Item
{...itemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Select
placeholder="选择或输入单位"
options={units}
showSearch
allowClear
onSearch={(value) => {
// 当输入的值不在现有选项中时,显示添加选项
if (value && !units.find(unit => unit.value === value)) {
const newOption = { value, label: `添加 "${value}"` };
setUnits([...units, newOption]);
}
}}
onSelect={(value, option) => {
// 如果选择的是新添加的选项
if (option.label.startsWith('添加 "')) {
addUnit(value);
}
}}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
);
};
return (
<Card
title={
@@ -313,12 +612,8 @@ const ServiceForm = () => {
showSearch
allowClear
mode="tags"
options={[
{ value: "VI设计", label: "VI设计" },
{ value: "平面设计", label: "平面设计" },
{ value: "网站建设", label: "网站建设" },
{ value: "营销推广", label: "营销推广" },
]}
options={categories}
loading={loading}
/>
</Form.Item>
<Form.Item
@@ -340,48 +635,42 @@ const ServiceForm = () => {
</div>
<Form.List name="sections">
{(fields, { add: addSection, remove: removeSection }) => (
{(fields, { add, remove }) => (
<>
<div className="space-y-6">
{fields.map((field, sectionIndex) => (
<Card
key={field.key}
className="!border-gray-200 dark:!border-gray-700 dark:bg-gray-800 hover:shadow-md transition-shadow duration-300"
className="!border-gray-200 dark:!border-gray-700
dark:bg-gray-800 hover:shadow-md transition-shadow duration-300"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 ">
<div className="flex items-center gap-2">
{editingSectionIndex === sectionIndex ? (
<div className="flex items-center gap-2">
<Input
placeholder="请输入小节名称"
className="!w-[200px]"
value={editingSectionName}
onChange={e => setEditingSectionName(e.target.value)}
onPressEnter={handleSectionNameSave}
autoFocus
/>
<Space className="flex items-center gap-2">
<Input
value={editingSectionName}
onChange={(e) => setEditingSectionName(e.target.value)}
onPressEnter={handleSectionNameSave}
autoFocus
className="w-48"
/>
<Button
type="primary"
size="small"
type="link"
icon={<CheckOutlined />}
onClick={handleSectionNameSave}
>
保存
</Button>
className="text-green-500 hover:text-green-600"
/>
<Button
size="small"
type="link"
icon={<CloseOutlined />}
onClick={handleSectionNameCancel}
>
取消
</Button>
</Space>
</div>
className="text-red-500 hover:text-red-600"
/>
</div>
) : (
<>
<Text
strong
className="text-lg dark:text-gray-200"
>
<div className="flex items-center gap-2">
<Text strong className="text-lg dark:text-gray-200">
{form.getFieldValue([
"sections",
sectionIndex,
@@ -390,7 +679,7 @@ const ServiceForm = () => {
</Text>
{(!id || isEdit) && (
<Button
type="text"
type="link"
icon={<EditOutlined />}
onClick={() =>
handleSectionNameEdit(
@@ -399,23 +688,37 @@ const ServiceForm = () => {
"sections",
sectionIndex,
"sectionName",
])
]) || `服务类型 ${sectionIndex + 1}`
)
}
size="small"
className="text-gray-400 hover:text-blue-500"
/>
)}
</>
</div>
)}
</div>
<Text className="text-gray-500 dark:text-gray-400">
合计:{" "}
{formatCurrency(
calculateSectionTotal(
formValues?.sections?.[sectionIndex]?.items
)
<div className="flex items-center gap-4">
{(!id || isEdit) && (
<Popconfirm
title="确认删除"
description="确定要删除这个小节吗?"
onConfirm={() => handleRemoveSection(sectionIndex)}
okText="确定"
cancelText="取消"
okButtonProps={{
className: "bg-red-500 hover:bg-red-600 border-red-500"
}}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
className="hover:text-red-500 transition-colors"
/>
</Popconfirm>
)}
</Text>
</div>
</div>
{/* 表头 */}
@@ -463,13 +766,7 @@ const ServiceForm = () => {
className="w-full"
/>
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Input placeholder="单位" />
</Form.Item>
{renderUnitSelect(itemField, sectionIndex, itemIndex)}
<Form.Item
{...itemField}
name={[itemField.name, "price"]}
@@ -538,12 +835,14 @@ const ServiceForm = () => {
</div>
{(!id || isEdit) && (
<div className="mt-6 flex gap-4 justify-center">
<div className="mt-6 flex justify-center">
<Button
type="dashed"
onClick={() => addSection({ items: [{}] })}
onClick={handleAddSection}
icon={<PlusOutlined />}
className="w-1/3 hover:border-blue-400 hover:text-blue-500 dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400"
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400
transition-all duration-300"
>
新建小节
</Button>
@@ -589,6 +888,25 @@ const ServiceForm = () => {
</Button>
</div>
)}
{/* 模版选择弹窗 */}
<Modal
title={
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
选择小节模版
</h3>
}
open={templateModalVisible}
onCancel={() => setTemplateModalVisible(false)}
footer={null}
width={800}
className="dark:bg-gray-800"
closeIcon={
<CloseOutlined className="text-gray-500 dark:text-gray-400" />
}
>
{renderTemplateModalContent()}
</Modal>
</Form>
</Card>
);