From d021f39b04020cedfe9956e232c5350e52aeb3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=98Liammcl=E2=80=99?= Date: Sat, 28 Dec 2024 15:51:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SectionList/index.jsx | 429 +++++++----- src/pages/company/quotation/detail/index.jsx | 326 +-------- src/pages/company/quotation/index.jsx | 60 +- .../detail/components/QuotationTemplate.jsx | 1 - src/pages/company/service/detail/index.jsx | 2 +- .../company/service/itemsManange/index.jsx | 37 +- .../service/itemsManange/sections/index.jsx | 662 ++++++++---------- .../resource/team/components/TeamTable.jsx | 1 - 8 files changed, 623 insertions(+), 895 deletions(-) diff --git a/src/components/SectionList/index.jsx b/src/components/SectionList/index.jsx index aea1352..e329549 100644 --- a/src/components/SectionList/index.jsx +++ b/src/components/SectionList/index.jsx @@ -1,26 +1,47 @@ -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'; +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"; import { supabase } from "@/config/supabase"; import { supabaseService } from "@/hooks/supabaseService"; const { Text } = Typography; -const SectionList = ({ +const SectionList = ({ form, isView, formValues, type, - currentCurrency = 'CNY' + currentCurrency = "TWD", }) => { const [editingSectionIndex, setEditingSectionIndex] = useState(null); - const [editingSectionName, setEditingSectionName] = useState(''); + 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 CURRENCY_SYMBOLS = { + CNY: "¥", + TWD: "NT$", + USD: "$", + }; - // 内部计算方法 const calculateItemAmount = (quantity, price) => { const safeQuantity = Number(quantity) || 0; const safePrice = Number(price) || 0; @@ -36,19 +57,13 @@ const SectionList = ({ }; const formatCurrency = (amount) => { - const CURRENCY_SYMBOLS = { - CNY: "¥", - TWD: "NT$", - USD: "$", - }; const safeAmount = Number(amount) || 0; - return `${CURRENCY_SYMBOLS[currentCurrency] || ""}${safeAmount.toLocaleString("zh-CN", { + return `${CURRENCY_SYMBOLS[currentCurrency] || "NT$"}${safeAmount.toLocaleString("zh-TW", { minimumFractionDigits: 2, maximumFractionDigits: 2, })}`; }; - // 获取可用的小节模板 const fetchAvailableSections = async () => { try { setLoading(true); @@ -56,6 +71,7 @@ const SectionList = ({ .from("resources") .select("*") .eq("type", "sections") + .eq("attributes->>template_type", [type]) .order("created_at", { ascending: false }); if (error) throw error; @@ -68,195 +84,235 @@ const SectionList = ({ } }; - // 处理小节名称编辑 const handleSectionNameEdit = (sectionIndex, initialValue) => { setEditingSectionIndex(sectionIndex); - setEditingSectionName(initialValue || ''); + setEditingSectionName(initialValue || ""); }; const handleSectionNameSave = () => { if (!editingSectionName.trim()) { - message.error('请输入小节名称'); + message.error("请输入小节名称"); return; } - const sections = form.getFieldValue('sections'); + const sections = form.getFieldValue("sections"); const newSections = [...sections]; newSections[editingSectionIndex] = { ...newSections[editingSectionIndex], - sectionName: editingSectionName.trim() + sectionName: editingSectionName.trim(), }; - form.setFieldValue('sections', newSections); + form.setFieldValue("sections", newSections); setEditingSectionIndex(null); - setEditingSectionName(''); + setEditingSectionName(""); }; - // 添加项目 const handleAddItem = (add) => { add({ key: uuidv4(), - name: '', - description: '', + name: "", + description: "", quantity: 1, price: 0, - unit: '' + unit: "", }); }; - // 处理使用模板 const handleUseTemplate = (template, add) => { const newSection = { key: uuidv4(), sectionName: template.attributes.name, - items: (template.attributes.items || []).map(item => ({ + items: (template.attributes.items || []).map((item) => ({ key: uuidv4(), - name: item.name || '', - description: item.description || '', + name: item.name || "", + description: item.description || "", price: item.price || 0, quantity: item.quantity || 1, - unit: item.unit || '', + unit: item.unit || "", })), }; add(newSection); setTemplateModalVisible(false); - message.success('套用模版成功'); + message.success("套用模版成功"); }; - // 处理创建自定义小节 const handleCreateCustom = (add, fieldsLength) => { add({ key: uuidv4(), sectionName: `服务类型 ${fieldsLength + 1}`, - items: [{ - key: uuidv4(), - name: '', - description: '', - quantity: 1, - price: 0, - unit: '' - }] + items: [ + { + key: uuidv4(), + name: "", + description: "", + quantity: 1, + price: 0, + unit: "", + }, + ], }); setTemplateModalVisible(false); }; - // 获取单位列表 const fetchUnits = async () => { setLoadingUnits(true); try { - const { data: units } = await supabaseService.select('resources', { + const { data: units } = await supabaseService.select("resources", { filter: { - 'type': { eq: 'units' }, - 'attributes->>template_type': { in: `(${type},common)` } + type: { eq: "units" }, + "attributes->>template_type": { in: `(${type},common)` }, }, order: { - column: 'created_at', - ascending: false - } + column: "created_at", + ascending: false, + }, }); setUnits(units || []); } catch (error) { - message.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', + const { error } = await supabase.from("resources").insert([ + { + type: "units", attributes: { - name: unitName + name: unitName, + template_type: type, }, - schema_version: 1 - }]); + schema_version: 1, + }, + ]); if (error) throw error; - message.success('新增单位成功'); + message.success("新增单位成功"); fetchUnits(); return true; } catch (error) { - message.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} 个项目 + {availableSections.length > 0 ? ( +
+
+ {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 + ) + )} + +
- -
- {(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 - ) - )} - -
-
+ ))}
- ))} -
- - -
- -
+
+ +
+
+ ) : ( +
+
+ + + +
+

+ 暂无可用模板 +

+

+ 您可以选择创建一个自定义模块开始使用 +

+ +
+ )}
); - // 修改项目小计的计算,将其封装为 memo 组件 const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => { const subtotal = useMemo(() => { const safeQuantity = Number(quantity) || 0; @@ -273,7 +329,6 @@ const SectionList = ({ ); }); - // 修改小节总计的计算,将其封装为 memo 组件 const SectionTotal = React.memo(({ items, currentCurrency }) => { const total = useMemo(() => { if (!Array.isArray(items)) return 0; @@ -281,7 +336,7 @@ const SectionList = ({ if (!item) return sum; const safeQuantity = Number(item.quantity) || 0; const safePrice = Number(item.price) || 0; - return sum + (safeQuantity * safePrice); + return sum + safeQuantity * safePrice; }, 0); }, [items]); @@ -302,11 +357,11 @@ const SectionList = ({ {(fields, { add, remove }) => ( <> -
+
{fields.map((field, sectionIndex) => ( @@ -315,7 +370,9 @@ const SectionList = ({
setEditingSectionName(e.target.value)} + onChange={(e) => + setEditingSectionName(e.target.value) + } onPressEnter={handleSectionNameSave} autoFocus className="w-48" @@ -331,7 +388,7 @@ const SectionList = ({ icon={} onClick={() => { setEditingSectionIndex(null); - setEditingSectionName(''); + setEditingSectionName(""); }} className="text-red-500" /> @@ -340,17 +397,26 @@ const SectionList = ({
- {form.getFieldValue(['sections', sectionIndex, 'sectionName']) - || `服务类型 ${sectionIndex + 1}`} + {form.getFieldValue([ + "sections", + sectionIndex, + "sectionName", + ]) || `服务类型 ${sectionIndex + 1}`} {!isView && (
} > - {/* 项目列表 */} {(itemFields, { add: addItem, remove: removeItem }) => ( <> - {/* 表头 */}
项目明细
描述/备注
@@ -383,7 +447,6 @@ const SectionList = ({
- {/* 项目列表 */} {itemFields.map((itemField, itemIndex) => (
({ + style={{ minWidth: "120px" }} + options={units.map((unit) => ({ label: unit.attributes.name, - value: unit.attributes.name + value: unit.attributes.name, }))} onDropdownVisibleChange={(open) => { if (open) fetchUnits(); @@ -423,48 +487,32 @@ const SectionList = ({ dropdownRender={(menu) => ( <> {menu} - - - - + /> +
)} /> @@ -474,18 +522,34 @@ const SectionList = ({ name={[itemField.name, "quantity"]} className="!mb-0" > - + - + - {!isView && itemFields.length > 1 && ( @@ -511,7 +575,7 @@ const SectionList = ({ )} - @@ -555,4 +619,5 @@ const SectionList = ({ ); }; -export default SectionList; \ No newline at end of file + +export default SectionList; diff --git a/src/pages/company/quotation/detail/index.jsx b/src/pages/company/quotation/detail/index.jsx index 13bcb72..e7a564a 100644 --- a/src/pages/company/quotation/detail/index.jsx +++ b/src/pages/company/quotation/detail/index.jsx @@ -18,16 +18,12 @@ import { ArrowLeftOutlined, SaveOutlined, DeleteOutlined, - CloseOutlined, - EditOutlined, - CheckOutlined, } from "@ant-design/icons"; import { supabase } from "@/config/supabase"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { v4 as uuidv4 } from "uuid"; - -const { TextArea } = Input; -const { Title, Text } = Typography; +import SectionList from '@/components/SectionList' +const { Title } = Typography; // 添加货币符号映射 const CURRENCY_SYMBOLS = { @@ -47,7 +43,7 @@ const QuotationForm = () => { const [dataSource, setDataSource] = useState([{ id: Date.now() }]); const [totalAmount, setTotalAmount] = useState(0); const [loading, setLoading] = useState(false); - const [currentCurrency, setCurrentCurrency] = useState("CNY"); + const [currentCurrency, setCurrentCurrency] = useState("TWD"); const [customers, setCustomers] = useState([]); const [selectedCustomers, setSelectedCustomers] = useState([]); const [formValues, setFormValues] = useState({}); @@ -737,304 +733,28 @@ const QuotationForm = () => {
- {/* 报价单细卡片 */} - - {(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) && ( -
- )} -
-
- } - > - - {(itemFields, { add: addItem, remove: removeItem }) => ( - <> - {/* 表头 */} -
-
项目明细
-
描述/备注
-
单位
-
数量
-
单价
-
小计
-
操作
-
- {itemFields.map((itemField, itemIndex) => ( -
- - - - - - - - - - - - - - - -
- - {formatCurrency( - calculateItemAmount( - formValues?.sections?.[sectionIndex] - ?.items?.[itemIndex]?.quantity, - formValues?.sections?.[sectionIndex] - ?.items?.[itemIndex]?.price - ) - )} - -
- {!isView && ( -
- ))} - - {!isView && ( - - )} - -
- - 小计总额: - - {formatCurrency( - calculateSectionTotal( - formValues?.sections?.[sectionIndex] - ?.items - ) - )} - - -
- - )} -
- - ))} -
- - {/* Add section button */} - {!isView && ( -
- -
- )} - - {/* 总金额统计 */} -
-
- 税前总计: - - {formatCurrency(calculateTotalAmount(formValues?.sections))} - -
- -
- 税率: -
- { - const value = e.target.value.replace(/[^\d]/g, ''); - setTaxRate(Number(value) || 0); - }} - /> -
-
- -
- 税后总计: - - {formatCurrency(afterTaxAmount)} - -
- -
- 折扣价: -
- { - const value = e.target.value.replace(/[^\d]/g, ''); - setDiscount(Number(value) || 0); - }} - /> -
-
- -
- 最终金额: - - {formatCurrency(discount || afterTaxAmount)} - -
-
- - - - 补充说明 - - } - bordered={false} - > - -