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
+ )
+ )}
+
+
+
+
+ ))}
+
+
+
+
+
+ }
+ onClick={() => handleCreateCustom(add, fieldsLength)}
+ className="w-1/3 border-2"
+ >
+ 自定义小节
+
+
+
+ );
+
+ return (
+ <>
+
+ {(fields, { add, remove }) => (
+ <>
+
+ {fields.map((field, sectionIndex) => (
+
+
+ {editingSectionIndex === sectionIndex ? (
+
+ setEditingSectionName(e.target.value)}
+ onPressEnter={handleSectionNameSave}
+ autoFocus
+ className="w-48"
+ />
+ }
+ onClick={handleSectionNameSave}
+ className="text-green-500"
+ />
+ }
+ onClick={() => {
+ setEditingSectionIndex(null);
+ setEditingSectionName('');
+ }}
+ className="text-red-500"
+ />
+
+ ) : (
+
+
+
+ {form.getFieldValue(['sections', sectionIndex, 'sectionName'])
+ || `服务类型 ${sectionIndex + 1}`}
+
+ {!isView && (
+ }
+ onClick={() => handleSectionNameEdit(
+ sectionIndex,
+ form.getFieldValue(['sections', sectionIndex, 'sectionName'])
+ )}
+ className="text-gray-400 hover:text-blue-500"
+ />
+ )}
+
+ )}
+
+ {!isView && (
+ }
+ onClick={() => remove(field.name)}
+ />
+ )}
+
+ }
+ >
+ {/* 项目列表 */}
+
+ {(itemFields, { add: addItem, remove: removeItem }) => (
+ <>
+ {/* 表头 */}
+
+
项目明细
+
描述/备注
+
单位
+
数量
+
单价
+
小计
+
+
+
+ {/* 项目列表 */}
+ {itemFields.map((itemField, itemIndex) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatCurrency(
+ calculateItemAmount(
+ formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity,
+ formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price
+ )
+ )}
+
+
+ {!isView && itemFields.length > 1 && (
+
}
+ onClick={() => removeItem(itemField.name)}
+ className="flex items-center justify-center"
+ />
+ )}
+
+ ))}
+
+ {!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 && (
-
- )}
-
-
-
-
- }
- 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"
- >
- 自定义小节
-
-
-
- );
-
- 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 (
-
-
- );
- };
-
return (
-
- {id ? (isEdit ? "编辑服务模版" : "查看服务模版") : "新增服务模版"}
-
- }
- className="dark:bg-gray-800"
- extra={
-
- }
- onClick={() => navigate("/company/serviceTeamplate")}
- className="flex items-center"
- >
- 返回列表
-
- {id && !isEdit && (
-
- )}
-
- }
- >
-
-
+ {/* 基本信息 */}
+
+
+ 基本信息
+
+ }
+ bordered={false}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 服务明细 */}
+
+
+ 服务明细
+
+ }
+ bordered={false}
+ >
+
+
+
+
+
);
};