From c5b2ae574bef541a85fa19c3f2e1bbb856936f1a Mon Sep 17 00:00:00 2001 From: liamzi Date: Thu, 26 Dec 2024 18:24:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=8A=BD=E7=A6=BB=E5=A4=8D?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SectionList/index.jsx | 527 ++++++++++++ src/pages/Dashboard.jsx | 2 +- src/pages/company/service/detail/index.jsx | 892 +++------------------ 3 files changed, 641 insertions(+), 780 deletions(-) create mode 100644 src/components/SectionList/index.jsx diff --git a/src/components/SectionList/index.jsx b/src/components/SectionList/index.jsx new file mode 100644 index 0000000..38168af --- /dev/null +++ b/src/components/SectionList/index.jsx @@ -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) => ( +
+
+ {availableSections.map(section => ( +
handleUseTemplate(section, add)} + > +
+
+

+ {section.attributes.name} +

+
+ {section.attributes.items?.length || 0} 个项目 +
+
+ +
+ {(section.attributes.items || []).slice(0, 3).map((item, index) => ( +
+ {item.name} + {formatCurrency(item.price)} +
+ ))} + {(section.attributes.items || []).length > 3 && ( +
+ 还有 {section.attributes.items.length - 3} 个项目... +
+ )} +
+ +
+ 总金额 + + {formatCurrency( + (section.attributes.items || []).reduce( + (sum, item) => sum + (item.price * (item.quantity || 1) || 0), + 0 + ) + )} + +
+
+
+ ))} +
+ + + +
+ +
+
+ ); + + return ( + <> + + {(fields, { add, remove }) => ( + <> +
+ {fields.map((field, sectionIndex) => ( + +
+ {editingSectionIndex === sectionIndex ? ( +
+ setEditingSectionName(e.target.value)} + onPressEnter={handleSectionNameSave} + autoFocus + className="w-48" + /> +
+ ) : ( +
+ + + {form.getFieldValue(['sections', sectionIndex, 'sectionName']) + || `服务类型 ${sectionIndex + 1}`} + + {!isView && ( +
+ )} +
+ {!isView && ( +
+ } + > + {/* 项目列表 */} + + {(itemFields, { add: addItem, remove: removeItem }) => ( + <> + {/* 表头 */} +
+
项目明细
+
描述/备注
+
单位
+
数量
+
单价
+
小计
+
+
+ + {/* 项目列表 */} + {itemFields.map((itemField, itemIndex) => ( +
+ + + + + + + + { + 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(); + } + } + }); + }} + > + 新增单位 + + + + )} + /> + + + + + + + +
+ + {formatCurrency( + calculateItemAmount( + formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity, + formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price + ) + )} + +
+ {!isView && itemFields.length > 1 && ( +
+ ))} + + {!isView && ( + + )} + +
+ + 小计总额: + + {formatCurrency( + calculateSectionTotal( + formValues?.sections?.[sectionIndex]?.items + ) + )} + + +
+ + )} +
+ + ))} + + + {!isView && ( +
+ +
+ )} + + 选择模版} + open={templateModalVisible} + onCancel={() => setTemplateModalVisible(false)} + footer={null} + width={800} + closeIcon={} + > + {renderTemplateModalContent(add, fields.length)} + + + )} +
+ + ); +}; + +export default SectionList; \ No newline at end of file diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 7834ffb..6146739 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -14,7 +14,7 @@ const Dashboard = () => { } }} > - + {/* */} ); diff --git a/src/pages/company/service/detail/index.jsx b/src/pages/company/service/detail/index.jsx index af6c4f9..d5daafe 100644 --- a/src/pages/company/service/detail/index.jsx +++ b/src/pages/company/service/detail/index.jsx @@ -1,29 +1,22 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect } from "react"; import { Card, Form, Input, - InputNumber, + Select, Button, Space, 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; +import SectionList from '@/components/SectionList'; + +const { Title } = Typography; const ServiceForm = () => { const [form] = Form.useForm(); @@ -32,23 +25,17 @@ const ServiceForm = () => { const [loading, setLoading] = useState(false); const location = useLocation(); const isEdit = location.search.includes("edit=true"); - const [editingSectionIndex, setEditingSectionIndex] = useState(null); - const [availableSections, setAvailableSections] = useState([]); const [formValues, setFormValues] = useState({ 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 () => { @@ -64,7 +51,7 @@ const ServiceForm = () => { const formData = { templateName: data.attributes.templateName, description: data.attributes.description, - category:data.attributes.category.map(v=>v.id), + category: data.attributes.category.map(v=>v.id), sections: data.attributes.sections, }; form.setFieldsValue(formData); @@ -77,22 +64,6 @@ const ServiceForm = () => { } }; - const fetchAvailableSections = async () => { - try { - 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); - } - }; - const fetchCategories = async () => { try { const { data: categoriesData, error } = await supabase @@ -104,7 +75,7 @@ const ServiceForm = () => { if (error) throw error; const formattedCategories = (categoriesData || []).map(category => ({ - value:category.id, + value: category.id, label: category.attributes.name })); @@ -115,39 +86,9 @@ const ServiceForm = () => { } }; - 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('获取单位数据失败'); - } - }; - const onFinish = async (values) => { try { setLoading(true); - const totalAmount = values.sections.reduce((sum, section) => { - return ( - sum + - (section.items || []).reduce((sectionSum, item) => { - return sectionSum + (item.quantity * item.price || 0); - }, 0) - ); - }, 0); const categoryData = values.category.map(categoryId => { const category = categories.find(c => c.value === categoryId); return { @@ -161,8 +102,7 @@ const ServiceForm = () => { templateName: values.templateName, description: values.description, sections: values.sections, - category: categoryData, - totalAmount, + category: categoryData, }, }; @@ -191,724 +131,118 @@ const ServiceForm = () => { } }; - const handleAddExistingSection = (sectionId) => { - const section = availableSections.find((s) => s.id === sectionId); - if (section) { - const sections = form.getFieldValue("sections") || []; - form.setFieldsValue({ - sections: [ - ...sections, - { - ...section.attributes, - sectionId: section.id, - }, - ], - }); - } - }; - - const currencyOptions = [ - { value: "CNY", label: "人民币 (¥)" }, - { value: "TWD", label: "台币 (NT$)" }, - { value: "USD", label: "美元 ($)" }, - ]; - - const calculateItemAmount = useMemo( - () => (quantity, price) => { - const safeQuantity = Number(quantity) || 0; - const safePrice = Number(price) || 0; - return safeQuantity * safePrice; - }, - [] - ); - - const calculateSectionTotal = useMemo( - () => - (items = []) => { - if (!Array.isArray(items)) return 0; - - return items.reduce((sum, item) => { - if (!item) return sum; - return sum + calculateItemAmount(item.quantity, item.price); - }, 0); - }, - [calculateItemAmount] - ); - - const calculateTotalAmount = useMemo( - () => - (sections = []) => { - if (!Array.isArray(sections)) return 0; - - return sections.reduce((sum, section) => { - if (!section) return sum; - return sum + calculateSectionTotal(section.items); - }, 0); - }, - [calculateSectionTotal] - ); - - const formatCurrency = useMemo( - () => - (amount, currency = form.getFieldValue("currency")) => { - const safeAmount = Number(amount) || 0; - const currencySymbol = - { - CNY: "¥", - TWD: "NT$", - USD: "$", - }[currency] || ""; - - return `${currencySymbol}${safeAmount.toLocaleString("zh-CN", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`; - }, - [] - ); - const handleValuesChange = (changedValues, allValues) => { setFormValues(allValues); }; - const [editingSectionName, setEditingSectionName] = useState(''); - - // 处理小节名称编辑 - 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 handleSectionNameCancel = () => { - setEditingSectionIndex(null); - 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 = () => ( -
-
- {availableSections.map(section => ( -
handleUseTemplate(section)} - > -
- {/* 标题和项目数量 */} -
-

- {section.attributes.name} -

-
- {section.attributes.items?.length || 0} 个项目 -
-
- - {/* 项目列表预览 */} -
- {(section.attributes.items || []).slice(0, 3).map((item, index) => ( -
- - {item.name} - - - {formatCurrency(item.price)} - -
- ))} - {(section.attributes.items || []).length > 3 && ( -
- 还有 {section.attributes.items.length - 3} 个项目... -
- )} -
- - {/* 小节总金额 */} -
- 总金额 - - {formatCurrency( - (section.attributes.items || []).reduce( - (sum, item) => sum + (item.price * (item.quantity || 1) || 0), - 0 - ) - )} - -
-
- - {/* 悬边框效果 */} -
- - {/* 选择指示器 */} -
- -
-
- ))} -
- - {availableSections.length === 0 && ( -
-
- 暂无可用模版 -
-
- )} - - - -
- -
-
- ); - - 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 ( - - - - - - - - - -
- - -
-
- - 报价明细 - - -
- - - {(fields, { add, remove }) => ( - <> -
- {fields.map((field, sectionIndex) => ( - -
-
- {editingSectionIndex === sectionIndex ? ( -
- setEditingSectionName(e.target.value)} - onPressEnter={handleSectionNameSave} - autoFocus - className="w-48" - /> -
- ) : ( -
- - {form.getFieldValue([ - "sections", - sectionIndex, - "sectionName", - ]) || `服务类型 ${sectionIndex + 1}`} - - {(!id || isEdit) && ( -
- )} -
-
- - {(!id || isEdit) && ( - handleRemoveSection(sectionIndex)} - okText="确定" - cancelText="取消" - okButtonProps={{ - className: "bg-red-500 hover:bg-red-600 border-red-500" - }} - > -
-
- - {/* 表头 */} -
-
项目明细
-
描述/备注
-
数量
-
单位
-
单价
-
小计
-
-
- - - {(itemFields, { add: addItem, remove: removeItem }) => ( - <> - {itemFields.map((itemField, itemIndex) => ( -
- - - - - - - - - - {renderUnitSelect(itemField, sectionIndex, itemIndex)} - - - -
- - {formatCurrency( - calculateItemAmount( - formValues?.sections?.[sectionIndex] - ?.items?.[itemIndex]?.quantity, - formValues?.sections?.[sectionIndex] - ?.items?.[itemIndex]?.price - ) - )} - -
- - {(!id || isEdit) && itemFields.length > 1 && ( -
- ))} - - {(!id || isEdit) && ( - - )} - -
- - 小计总额: - - {formatCurrency( - calculateSectionTotal( - formValues?.sections?.[sectionIndex] - ?.items - ) - )} - - -
- - )} -
-
- ))} -
- - {(!id || isEdit) && ( -
- -
- )} - - {/* 总金额统计 */} -
-
- -
- - 总金额 - - - {formatCurrency( - calculateTotalAmount(formValues?.sections) - )} - -
-
-
- - )} -
-
- - {(!id || isEdit) && ( -
- - -
- )} - - {/* 模版选择弹窗 */} - - 选择小节模版 - - } - open={templateModalVisible} - onCancel={() => setTemplateModalVisible(false)} - footer={null} - width={800} - className="dark:bg-gray-800" - closeIcon={ - - } +
- {renderTemplateModalContent()} - -
- + {/* 基本信息 */} + + + 基本信息 + + } + bordered={false} + > +
+ + + + +